Hybrid Search
We will show you how to use pgvecto.rs to implement hybrid search in this section.
Vector Search and Full Text Search
Postgres provides full text search, based on the match operator @@. It returns true if a tsvector (document) matches a tsquery (query). It doesn’t matter which data type is written first:
As the above example suggests, a tsquery is not just raw text, any more than a tsvector is. A tsquery contains search terms, which must be already-normalized lexemes, and may combine multiple terms using AND, OR, NOT, and FOLLOWED BY operators. In this example, the tsquery 'cat & rat' matches the tsvector 'a fat cat sat on a mat and ate a fat rat' because it contains both the terms 'cat' and 'rat'.
You could use full text search and vector search together in a single query, with the help of pgvecto.rs. In this example, we create a table named items with two columns: content and embedding. The column content is of type text, and the column embedding is of type vector(3). We insert four items into the table:
You should get the following table:
Let’s search for the items with the word cat and rat in their content first. We will use full text search to do that. The function to_tsvector converts a text to a tsvector. The operator @@ checks if a tsvector matches a tsquery.
to_tsvector parses a textual document into tokens, reduces the tokens to lexemes, and returns a tsvector which lists the lexemes together with their positions in the document.
In the example above we see that the resulting tsvector does not contain the words a, on, or it, the word rats became rat, and the punctuation sign - was ignored, because of the text search configuration is set to english.
We should get the following result:
Then we will sort the result by the distance between the embedding of each item and the embedding of the query. We will use vector search to do that:
We will get the item with the embedding [7, 8, 9] first, and the item with the embedding [1, 2, 3] second:
Advanced search: Merge the results of full text search and vector search
You could normalize the rank of the full text search result and the vector search result, and then merge them together:
The SQL query performs a combined semantic search and full-text search on “items” and retrieves the results based on their relevance scores. Here’s a breakdown of the query:
- The query starts with a Common Table Expression (CTE) named “semantic_search.” It selects columns “id,” “content,” “embedding,” and calculates the rank using the “<->” operator, which measures the distance between the “embedding” column and the vector ‘[7, 8, 9]’. The results are ordered by the rank.
- Another CTE named “full_text_search” is defined. It selects the same columns as the previous CTE but calculates the rank using the “ts_rank” function with a full-text search query. The ’to_tsvector’ function converts the “content” column into a tsvector, and the ’tsquery’ ‘cat & rat’ matches rows containing both “cat” and “rat” in the “content” column. The results are ordered by the rank.
- The main query selects columns from both CTEs using the
COALESCEfunction to handle cases where one CTE may have null values. It also calculates the overall rank by summing the reciprocal of the ranks from each CTE. - The main query performs a FULL OUTER JOIN between the two CTEs on the “id” column.
- The final result is ordered by the overall rank in descending order.
In this query, we first calculate the rank of the full text search result and the vector search result separately. Then we merge the two results together. We will get the following result: