Site icon Bugra Parlayan | Oracle Database Blog

Oracle Text vs REGEXP_LIKE: Choosing the Right Full-Text Search in Oracle

Introduction: The Challenge of Text Search in Oracle Databases

Searching through large volumes of text data stored within relational databases presents unique challenges. Unlike structured data, where traditional B-tree indexes provide efficient lookup based on exact values or ranges, text data often requires searching for words, phrases, or patterns within unstructured or semi-structured content. Standard SQL operators are often insufficient or inefficient for these tasks. Oracle Database offers two primary built-in mechanisms to address this challenge: standard SQL regular expressions, primarily accessed through the REGEXP_LIKE condition, and the more specialized, feature-rich Oracle Text component.  

REGEXP_LIKE provides powerful pattern-matching capabilities conforming to POSIX standards, allowing developers to validate data formats or find specific character sequences within strings. Oracle Text, on the other hand, is a dedicated full-text retrieval engine designed for indexing and querying large document collections, offering advanced linguistic features, relevance ranking, and optimized performance through specialized index structures.  

This document provides a comprehensive technical comparison of Oracle’s REGEXP_LIKE function and the Oracle Text CONTEXT index type with its associated CONTAINS operator. It aims to equip Oracle Database Administrators (DBAs), developers, and architects with the necessary understanding of their respective functionalities, architectures, performance characteristics, maintenance requirements, and ideal use cases, enabling informed decisions when implementing text search capabilities within Oracle Database environments.

Understanding SQL Regular Expressions (REGEXP_LIKE) in Oracle

Oracle’s SQL regular expression functions provide powerful tools for pattern matching directly within SQL statements. The REGEXP_LIKE condition is central to this, allowing users to check if a string conforms to a specified pattern.

Functionality and Syntax

Core Purpose: The fundamental role of REGEXP_LIKE is to serve as a condition within SQL queries (typically in WHERE or HAVING clauses) to determine if a source string matches a given regular expression pattern. It returns a boolean-like result (implicitly, as SQL doesn’t have a true boolean type; it’s used in conditions evaluating to true or false). Its primary function is pattern matching and validation, rather than the content retrieval and relevance ranking focus of Oracle Text.  

Syntax Breakdown: The function follows the syntax: REGEXP_LIKE (source_char, pattern [, match_param])

Metacharacters: REGEXP_LIKE supports a set of common POSIX-compliant metacharacters. It’s important to note that the interpretation of metacharacters can differ slightly between various tools and platforms supporting regular expressions. Developers porting expressions should verify syntax and behavior.  

Table 1: Common REGEXP_LIKE Metacharacters

Metacharacter SyntaxOperator NameDescriptionReference
.Any Character (Dot)Matches any single character (except newline unless ‘n’ param is used)
+One or More QuantifierMatches one or more occurrences of the preceding subexpression.
?Zero or One QuantifierMatches zero or one occurrence of the preceding subexpression.
*Zero or More QuantifierMatches zero or more occurrences of the preceding subexpression.
{m}Interval (Exact Count)Matches exactly m occurrences of the preceding subexpression.
{m,}Interval (At Least Count)Matches at least m occurrences of the preceding subexpression.
{m,n}Interval (Between Count)Matches at least m, but not more than n occurrences.
[...]Matching Character ListMatches any single character within the list (e.g., [abc]).
[^...]Non-Matching Character ListMatches any single character not within the list (e.g., [^abc]).
`\`OrMatches either the expression before or the expression after the pipe.
(...)Subexpression / GroupingGroups subexpressions; treated as a unit.
\nBackreferenceMatches the nth preceding subexpression (n=1-9).
\Escape CharacterTreats the subsequent metacharacter as a literal.
^Beginning of Line AnchorMatches the expression only at the beginning of a line/string.
$End of Line AnchorMatches the expression only at the end of a line/string.
[:class:]POSIX Character ClassMatches any character belonging to the specified class (e.g., [:digit:]).

Anchoring Behavior: Unlike some regular expression engines where REGEXP_LIKE might imply matching the entire string by default, Oracle’s implementation behaves more like a substring search unless explicit anchors (^ for start, $ for end) are included in the pattern. For example, REGEXP_LIKE(col, 'ABC') will match rows where col contains ‘ABC’ anywhere, whereas REGEXP_LIKE(col, '^ABC$') requires col to be exactly ‘ABC’. This implicit behavior is crucial for developers to understand to avoid unexpected matches.  

Practical Examples

The flexibility of REGEXP_LIKE is best illustrated through examples:

Pros and Cons of REGEXP_LIKE for Text Search

While powerful for pattern matching, REGEXP_LIKE has significant drawbacks when used for general-purpose text search, especially compared to Oracle Text.

Pros:

Cons:

The apparent ease of using REGEXP_LIKE for complex text pattern searches can be deceptive. Its default execution path is often a full table scan, leading to poor performance on large datasets. While Function-Based Indexes offer a potential optimization path, they introduce their own set of complexities, strict matching requirements, and potential pitfalls (like non-determinism issues). Often, the performance gain from an FBI is insufficient to match the efficiency of a dedicated text indexing solution like Oracle Text, making REGEXP_LIKE generally unsuitable for high-performance, large-scale text retrieval tasks.  

Deep Dive into Oracle Text (CTXSYS.CONTEXT Index and CONTAINS)

Oracle Text is a powerful technology integrated within the Oracle Database, providing sophisticated indexing and search capabilities for text data. It goes far beyond simple pattern matching, offering features optimized for document retrieval, linguistic analysis, and relevance ranking.  

Architecture and Core Concepts

Overview and History: Oracle Text enables applications to intelligently search and analyze text documents stored within the database or in external sources. Its capabilities have evolved over several Oracle versions. It originated as Oracle ConText, introduced around 1997 with Oracle8. It was subsequently rebranded as Oracle interMedia Text in Oracle8i (circa 1999) before becoming the fully integrated Oracle Text feature included with Oracle9i (circa 2001) and available in all database editions since. This history signifies a mature, deeply integrated technology within the database kernel.  

The CONTEXT Index: Oracle Text offers several index types, including CONTEXT, CTXCAT, and CTXRULE. The CONTEXT index type is specifically designed for indexing large, coherent documents, such as articles, reports, books, or web pages. It supports a wide range of document formats (including Microsoft Word, PDF, HTML, XML, plain text) stored in various Oracle data types (CLOB, BLOB, BFILE, VARCHAR2, CHAR, XMLType) or referenced via paths or URLs in table columns.  

Indexing Process Components: Creating a CONTEXT index involves a sophisticated pipeline that processes documents to make them searchable :  

  1. Datastore: Defines where the documents are stored. Options include DIRECT_DATASTORE (text in a table column), MULTI_COLUMN_DATASTORE (text spread across multiple columns, concatenated into a virtual document), FILE_DATASTORE (path to OS files stored in a column), URL_DATASTORE (URLs stored in a column), and others.  
  2. Filter: Defines how documents are converted into plain text for indexing. AUTO_FILTER attempts to automatically detect the format (e.g., PDF, Word) and apply the correct filter. NULL_FILTER is used for already plain text documents. CHARSET_FILTER handles character set conversions.  
  3. Sectioner: Divides the document text into logical sections based on internal structure (like HTML or XML tags). This enables targeted searches using the WITHIN operator. Examples include HTML_SECTION_GROUP and XML_SECTION_GROUP.  
  4. Lexer: Breaks the filtered text into individual words or tokens based on language-specific rules. It handles whitespace, punctuation, and case conversion. Language-specific lexers (e.g., CHINESE_LEXER, JAPANESE_LEXER) handle languages without clear word separators. The BASIC_LEXER is common for whitespace-delimited languages. Theme indexing can also occur here.  
  5. Wordlist: Defines preferences for advanced query features like stemming (finding related word forms), fuzzy matching (finding similar spellings), and prefix/substring indexing. BASIC_WORDLIST is the default.  
  6. Stoplist: Specifies a list of common words (like “the”, “a”, “is”) or themes (stopthemes) that should be ignored during indexing to save space and improve relevance. BASIC_STOPLIST is the default for the database language.  
  7. Storage: Defines the physical storage characteristics (tablespaces, storage parameters) for the internal database objects that constitute the Oracle Text index. BASIC_STORAGE is the default.  

The Inverted Index: The core output of the indexing process is an inverted index. Instead of mapping a document to its words, it maps each unique token (word) back to the list of documents that contain it, including positional information (where the word occurs within each document). This structure is fundamental to the high performance of Oracle Text searches, allowing the engine to quickly identify matching documents without scanning the entire text collection.

Internal Index Tables: A CONTEXT index is not a single object but a collection of internal tables and indexes managed by Oracle Text within the CTXSYS schema, typically prefixed with DR$indexname$ :  

The intricate architecture involving a multi-stage indexing pipeline and a set of specialized internal tables contrasts sharply with the simple functional execution of REGEXP_LIKE. This architectural complexity, however, is precisely what underpins Oracle Text’s power. It enables the handling of diverse document formats, sophisticated linguistic processing, section searching, and ultimately, the creation of the highly efficient inverted index structure that makes rapid text retrieval possible.  

Indexing and Maintenance

Creating and maintaining a CONTEXT index involves specific procedures distinct from standard Oracle index management.

Index Creation: A CONTEXT index is created using the standard CREATE INDEX statement, but with the INDEXTYPE IS CTXSYS.CONTEXT clause.  

SQL

-- Basic CONTEXT index creation
CREATE INDEX my_text_index ON documents (text_column)
  INDEXTYPE IS CTXSYS.CONTEXT;

-- Creation with custom parameters (e.g., specifying a custom lexer and stoplist)
CREATE INDEX my_custom_index ON documents (text_column)
  INDEXTYPE IS CTXSYS.CONTEXT
  PARAMETERS ('LEXER my_custom_lexer STOPLIST my_custom_stoplist STORAGE my_storage_pref');

By default, Oracle Text makes assumptions about the data source, format, language, etc.. The optional PARAMETERS clause allows customization by specifying pre-defined preferences for datastore, filter, lexer, wordlist, stoplist, section group, and storage objects. For very large tables, indexing can be deferred using the NOPOPULATE parameter during creation. The CTX_DDL.POPULATE_PENDING procedure can then be used to mark all rows for indexing, which are subsequently processed by CTX_DDL.SYNC_INDEX.  

Synchronization (CTX_DDL.SYNC_INDEX): Unlike standard B-tree indexes or even the CTXCAT index type , a CONTEXT index is not automatically updated transactionally when DML (INSERT, UPDATE, DELETE) occurs on the base table. Instead, changes are recorded in a pending queue. The CTX_DDL.SYNC_INDEX procedure must be explicitly called to process these pending changes and update the text index.  

SQL

-- Synchronize the index 'my_text_index' using default memory
BEGIN
  CTX_DDL.SYNC_INDEX('my_text_index');
END;
/

-- Synchronize with 16MB memory allocation
BEGIN
  CTX_DDL.SYNC_INDEX('my_text_index', '16M');
END;
/

-- Synchronize for a maximum of 60 minutes
BEGIN
  CTX_DDL.SYNC_INDEX('my_text_index', '2M', maxtime => 60); -- maxtime in minutes
END;
/

Synchronization frequency depends on application requirements for search timeliness. It can be scheduled periodically (e.g., via DBMS_SCHEDULER). Alternatively, automatic synchronization can be configured during index creation using PARAMETERS ('SYNC (ON COMMIT)') or PARAMETERS ('SYNC (EVERY "SYSDATE+1/24")'). However, SYNC (ON COMMIT) can introduce performance overhead on DML operations and is often discouraged for high-throughput systems. Failure to synchronize regularly will result in queries returning stale results, as the index will not reflect recent data changes.  

Optimization (CTX_DDL.OPTIMIZE_INDEX): Frequent DML and synchronization can lead to index fragmentation, where the same token might have multiple entries in the $I table, potentially slowing down query performance. Deletions also leave “dead” entries in the index until optimization. The CTX_DDL.OPTIMIZE_INDEX procedure reorganizes the index structure to reduce fragmentation and remove deleted document references.  

SQL

-- Perform a full optimization (includes garbage collection)
BEGIN
  CTX_DDL.OPTIMIZE_INDEX('my_text_index', 'FULL');
END;
/

-- Perform a faster, less thorough optimization
BEGIN
  CTX_DDL.OPTIMIZE_INDEX('my_text_index', 'FAST');
END;
/

-- Optimize only a specific token (useful for frequently searched terms)
BEGIN
  CTX_DDL.OPTIMIZE_INDEX('my_text_index', 'TOKEN', token => 'oracle');
END;
/

Optimization can be resource-intensive, especially FULL optimization, and is typically scheduled during off-peak hours. Oracle 12c introduced the AUTO_OPTIMIZE feature (CTX_DDL.ADD_AUTO_OPTIMIZE), which allows indexes to be registered for automatic background optimization, simplifying maintenance. Neglecting optimization can lead to gradual query performance degradation over time.  

Rebuilding/Recreating: For major changes to index preferences or significant reorganization, ALTER INDEX... REBUILD PARAMETERS(...) or the online CTX_DDL.RECREATE_INDEX_ONLINE procedure can be used.  

The requirement for explicit synchronization and optimization represents a key operational difference and overhead compared to REGEXP_LIKE, which needs no such index-specific maintenance. This maintenance cycle is critical: synchronization ensures data accuracy in search results, while optimization ensures sustained query performance. Omitting this maintenance can severely undermine the benefits of using Oracle Text.  

The CONTAINS Operator

The primary interface for querying a CONTEXT index is the CONTAINS SQL operator.  

Core Usage: CONTAINS is used within the WHERE clause of a SELECT statement. Its basic syntax requires a positive score check: CONTAINS(column_name, text_query [, label]) > 0

The > 0 condition is mandatory because CONTAINS returns a relevance score (0-100) for each row, and only rows with a score greater than zero match the query. The SCORE(label) operator is typically used in the SELECT list or ORDER BY clause to retrieve or sort by this relevance score.  

SQL

SELECT SCORE(1) as relevance, title
FROM news
WHERE CONTAINS(text, 'database performance', 1) > 0
ORDER BY SCORE(1) DESC;

Query Operators (CONTEXT Grammar): The power of CONTAINS lies in its rich set of query operators within the text_query string, often referred to as the CONTEXT grammar. These operators enable sophisticated searches far beyond simple keyword matching :  

Table 2: Key CONTAINS Query Operators

OperatorExample SyntaxDescriptionReference
Logical
AND'oracle & database'Finds documents containing both ‘oracle’ AND ‘database’.
OR`’oracletext’`Finds documents containing either ‘oracle’ OR ‘text’.
NOT'oracle ~ text'Finds documents containing ‘oracle’ but NOT ‘text’.
ACCUMulate'oracle, text'Finds documents containing ‘oracle’ OR ‘text’, scores are accumulated.
MINUS'oracle - text'Similar to NOT, reduces score if the subtracted term is present.
Proximity
NEAR'NEAR((oracle, database), 10)'Finds ‘oracle’ and ‘database’ within 10 words of each other, order independent.
NEAR (ordered)'NEAR((oracle, database), 10, TRUE)'Finds ‘oracle’ then ‘database’ within 10 words, order dependent.
Linguistic
Stem ($)'$running'Finds words with the same stem as ‘running’ (e.g., run, ran).
Fuzzy (?)'?government'Finds words spelled similarly to ‘government’.
Soundex (!)'!smith'Finds words that sound like ‘smith’ (e.g., smythe).
Wildcards
%'comp%'Matches words starting with ‘comp’ (e.g., computer, component).
_'comp_te'Matches words like ‘compete’, ‘compute’ (single character wildcard).
Thesaurus(Requires a configured thesaurus)
SYN'SYN(car)'Expands to include synonyms of ‘car’ (e.g., automobile).
BT'BT(dog)'Expands to include broader terms of ‘dog’ (e.g., canine).
NT'NT(cat)'Expands to include narrower terms of ‘cat’ (e.g., kitten).
Scoring
Weight (*)'oracle*3'Increases the score contribution of the term ‘oracle’.
Threshold (>)'oracle > 50'Matches ‘oracle’ only if its individual score contribution is > 50.
Equivalence (=)'db = database'Treats ‘db’ and ‘database’ as equivalent terms for matching and scoring.
Theme/Section
ABOUT'ABOUT(finance)'Finds documents whose theme is about ‘finance’, even if the word isn’t present.
WITHIN'oracle WITHIN title'Finds ‘oracle’ only within sections defined as ‘title’.
XML(Requires PATH_SECTION_GROUP)
HASPATH'HASPATH(/book/author)'Finds documents containing the specified XML path.
INPATH'oracle INPATH (//section)'Finds ‘oracle’ within elements matching the specified path expression.

SQL Examples:

Pros and Cons of Oracle Text (CONTEXT/CONTAINS)

Oracle Text offers significant advantages for text searching but also comes with its own set of considerations.

Pros:

Cons:

Performance Showdown: REGEXP_LIKE vs. CONTAINS

The performance difference between REGEXP_LIKE and CONTAINS is often the most critical factor in choosing between them for text search tasks. This difference stems primarily from their underlying indexing mechanisms and query execution strategies.

Indexing Impact

CONTEXT Index: The CONTEXT index employs an inverted index structure, primarily stored in the $I table. This structure maps each unique word (token) to a list of documents (and positions within those documents) where it occurs. When a CONTAINS query searches for ‘database’, Oracle Text can directly look up ‘database’ in the $I index (using the associated $X B-tree index for speed) and immediately retrieve the list of matching document ROWIDs from the $R table. This avoids scanning rows that do not contain the search term. Index optimization (OPTIMIZE_INDEX) is crucial for maintaining this efficiency by reducing fragmentation in the $I table.  

REGEXP_LIKE Indexing: REGEXP_LIKE typically forces a Full Table Scan (FTS) because standard B-tree indexes are designed for prefix-based or range-based lookups, not arbitrary pattern matching within a string. A B-tree index on a VARCHAR2 column can only be effectively used by LIKE if the pattern starts with fixed characters (e.g., LIKE 'ABC%'). Patterns with leading wildcards (LIKE '%ABC') or complex regular expressions generally render standard indexes useless for REGEXP_LIKE.  

The alternative is a Function-Based Index (FBI), for example, CREATE INDEX idx_name ON table (REGEXP_LIKE(column, pattern)). However, this approach is fraught with limitations :  

  1. The WHERE clause in the query must exactly match the expression used in the FBI definition.
  2. The optimizer needs current statistics to even consider the FBI.
  3. The optimizer might still choose FTS if it deems the FBI scan too expensive (e.g., if the pattern is not selective).
  4. Some regular expressions might be considered non-deterministic, preventing index creation.  
  5. Even when used, the FBI might only offer marginal improvement over FTS, as the database still needs to evaluate the regular expression for potentially many index entries.

The fundamental difference lies in the index design’s suitability for the task. CONTEXT uses an index specifically built for efficient text token lookup (inverted index). REGEXP_LIKE attempts to retrofit pattern matching onto indexes designed for value lookup (B-tree, potentially via an FBI), which is inherently less efficient for the types of searches typically performed on text content.  

Query Execution Speed

For most text search scenarios involving large datasets, CONTAINS with a CONTEXT index consistently and significantly outperforms REGEXP_LIKE.  

The Oracle Cost-Based Optimizer (CBO) plays a role. Oracle Text allows for the collection of domain index statistics, which helps the CBO accurately estimate the cost of using the CONTEXT index versus an FTS. This leads to more intelligent plan choices. The CBO often struggles to accurately estimate the cost and selectivity of complex REGEXP_LIKE predicates, frequently defaulting to the perceived safety of an FTS unless a perfectly matching, highly selective FBI is available.  

Scalability Considerations

CONTEXT: Designed for scalability. Performance generally scales well with data volume, provided the index is properly maintained (synchronized and optimized). Partitioning the base table and creating LOCAL CONTEXT indexes can further enhance parallelism and manageability for very large tables.  

REGEXP_LIKE: Scales very poorly. As the table size grows, the time required for an FTS increases linearly. CPU becomes a major bottleneck as the regex engine must be applied to every row (or a large portion of rows/index entries if an inefficient FBI is used).

It is crucial to recognize that the scalability of CONTEXT indexes is directly tied to their maintenance. An unmaintained CONTEXT index (not synchronized or optimized) will suffer performance degradation over time due to stale data and fragmentation. In extreme cases of neglect, query performance could potentially become very poor. Thus, the operational commitment to maintenance is an inherent factor in achieving scalability with Oracle Text.

Use Case Guidance: When to Choose REGEXP_LIKE vs. Oracle Text

The choice between REGEXP_LIKE and Oracle Text (CONTEXT/CONTAINS) depends heavily on the specific requirements of the application, particularly concerning performance, data volume, and the nature of the search.

Favorable Scenarios for REGEXP_LIKE:

  1. Data Validation: Ideal for checking if string data conforms to a predefined format, such as validating email addresses, phone numbers, postal codes, or specific ID structures. Here, the goal is pattern conformance, not content retrieval.  
  2. Simple, Ad-hoc Pattern Matching: Useful for quick, infrequent searches for specific, known patterns on smaller tables where setting up and maintaining a full Oracle Text index would be excessive overhead.
  3. Targeted Substring Extraction/Matching: When the need is to find or validate a very specific sequence of characters within a larger string, and the flexibility of regex is required (e.g., finding patterns that span across potential word boundaries defined by the Text lexer).
  4. Environments Where Oracle Text is Unavailable/Undesirable: If Oracle Text is not licensed (though it’s typically included) or if the administrative overhead of maintaining Text indexes is deemed unacceptable for the specific use case.

Favorable Scenarios for Oracle Text (CONTEXT/CONTAINS):

  1. Large Document Collection Search: The quintessential use case – indexing and searching extensive collections of documents (Word, PDF, HTML, text files, XML) based on their textual content.  
  2. Website/Intranet Search Engines: Indexing web pages or internal documents for keyword-based retrieval.  
  3. Content Management Systems: Providing search capabilities within systems managing articles, reports, or other textual assets.
  4. E-commerce Catalog Search (Advanced): While CTXCAT is often preferred for simple catalog searches , CONTEXT is suitable if advanced features like stemming, fuzzy matching, or theme analysis are required alongside structured product attributes.  
  5. Applications Requiring Relevance Ranking: When search results need to be ordered based on relevance to the user’s query, using the SCORE operator.  
  6. Applications Benefiting from Linguistic Features: When searches must accommodate variations in spelling (fuzzy matching), word forms (stemming), or related concepts (thesaurus support).  
  7. Searching Within Document Structure: When queries need to target specific sections within HTML or XML documents using WITHIN, HASPATH, or INPATH operators.  
  8. High-Performance & Scalable Text Search: Any application where fast response times and scalability are critical for searching large volumes of text data under concurrent user load.

A core distinction often lies in the intent: validation versus retrieval. REGEXP_LIKE primarily validates if a string matches a pattern. CONTAINS primarily retrieves documents based on the semantic concepts expressed in the query, often ranking them by relevance. Understanding this fundamental difference helps guide the selection process. If the task is “does this string look like an email address?”, REGEXP_LIKE is appropriate. If the task is “find me all documents discussing database performance optimization”, CONTAINS with a CONTEXT index is the vastly superior choice.  

Oracle Text Version History

Oracle Text is a mature feature with a history spanning several major Oracle Database releases:

Since Oracle9i, Oracle Text has been continuously supported and enhanced across subsequent major releases (10g, 11g, 12c, 18c, 19c, 21c, 23ai, etc.), solidifying its position as Oracle’s strategic technology for advanced text search and analysis within the database. This long history underscores its maturity, stability, and deep integration with the database kernel, contrasting with simpler functions like REGEXP_LIKE which, while useful, lack the specialized architecture and decades of development focused purely on text retrieval challenges.

Conclusion: Optimizing Text Search in Your Oracle Database

Choosing the appropriate mechanism for text searching within Oracle Database is crucial for application performance and functionality. Both SQL regular expressions (REGEXP_LIKE) and Oracle Text (CONTEXT index with CONTAINS) offer distinct capabilities tailored to different needs.

Summary: REGEXP_LIKE provides a flexible, SQL-standard way to perform complex pattern matching and validation within strings. However, its performance for searching content within large datasets is often poor due to reliance on full table scans or the limitations of Function-Based Indexes. Oracle Text, specifically using the CONTEXT index type and the CONTAINS operator, is a dedicated text retrieval engine. It employs specialized inverted indexing and a rich query language to deliver high-performance, scalable, and feature-rich searching (including relevance ranking, linguistic variations, and section searching) for document collections and similar text-heavy applications.

Key Takeaways:

Recommendations:

Final Thought: Oracle Database provides distinct tools optimized for different types of string searching. REGEXP_LIKE is a powerful tool for pattern matching, while Oracle Text (CONTEXT/CONTAINS) is the optimized solution for text retrieval. Selecting the right tool based on the specific application requirements – whether validating a format or searching a library of documents – is fundamental to building efficient, scalable, and functionally rich Oracle applicatio

Exit mobile version