vecs GitHub issue #88 seems to suggest that it's not supported and it won't be simply because the vendor/maintainer, Supabase, meant it as semantic-only.
vecs is a lib for semantic search. If you're interested in hybrid search I'd suggest following out hybrid search docs which explain how to create an appropriate SQL function and access it via the REST API
And following that link leads to Supabase's take on pgvector's hybrid search example, which runs two separate searches, one using full-text search GIN index, another using HNSW, then combines and reranks the results:
WITH semantic_search AS (
SELECT id, RANK () OVER (ORDER BY embedding <=> %(embedding)s) AS rank
FROM documents
ORDER BY embedding <=> %(embedding)s
LIMIT 20
),
keyword_search AS (
SELECT id, RANK()OVER(ORDER BY ts_rank_cd(to_tsvector('english',content),query) DESC)
FROM documents, plainto_tsquery('english', %(query)s) query
WHERE to_tsvector('english', content) @@ query
ORDER BY ts_rank_cd(to_tsvector('english', content), query) DESC
LIMIT 20
)
SELECT
COALESCE(semantic_search.id, keyword_search.id) AS id,
COALESCE(1.0 / (%(k)s + semantic_search.rank), 0.0) +
COALESCE(1.0 / (%(k)s + keyword_search.rank), 0.0) AS score
FROM semantic_search
FULL OUTER JOIN keyword_search ON semantic_search.id = keyword_search.id
ORDER BY score DESC
LIMIT 5
Pinecone seems to recommend the same thing:
dense_results = dense_index.search(
namespace="example-namespace",
query={ "top_k": 40,
"inputs": {"text": query}
}
)
sparse_results = sparse_index.search(
namespace="example-namespace",
query={ "top_k": 40,
"inputs": {"text": query}
}
)
def merge_chunks(h1, h2):
"""Get the unique hits from two search results and return them as single array of {'_id', 'chunk_text'} dicts, printing each dict on a new line."""
# Deduplicate by _id
deduped_hits = {hit['_id']: hit for hit in h1['result']['hits'] + h2['result']['hits']}.values()
# Sort by _score descending
sorted_hits = sorted(deduped_hits, key=lambda x: x['_score'], reverse=True)
# Transform to format for reranking
result = [{'_id': hit['_id'], 'chunk_text': hit['fields']['chunk_text']} for hit in sorted_hits]
return result
merged_results = merge_chunks(sparse_results, dense_results)
result = pc.inference.rerank(
model="bge-reranker-v2-m3",
query=query,
documents=merged_results,
rank_fields=["chunk_text"],
top_n=10,
return_documents=True,
parameters={"truncate": "END"}
)
That's what Vespa does as well:
These top-k query operators use index structures to accelerate the query evaluation, avoiding scoring all documents using heuristics. In the context of hybrid text search, the following Vespa top-k query operators are relevant:
- YQL
{targetHits:k}nearestNeighbor() for dense representations (text embeddings) using a configured distance-metric as the scoring function.
- YQL
{targetHits:k}userInput(@user-query) which by default uses weakAnd for sparse representations.
We can combine these operators using boolean query operators like AND/OR/RANK to express a hybrid search query. Then, there is a wild number of ways that we can combine various signals in ranking.
vecswrapspgvectorand you can do hybrid search with that, it's a reasonable expectation that it might be possible. I don't think an MRE is a requirement here either. I think it's suitable for SO, and the fact that some version of this question could be also suitable for dba, stats, softwareengineering, cs, datascience and softwarerecs is no reason to close