Skip to content

Commit 65016f7

Browse files
authored
feat: Support for hyperscale and composite index (#17)
1 parent 20375ca commit 65016f7

27 files changed

Lines changed: 3243 additions & 271 deletions

README.md

Lines changed: 355 additions & 53 deletions
Large diffs are not rendered by default.

examples/gsi/indexing_pipeline.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import json
2+
import logging
3+
import os
4+
import zipfile
5+
from datetime import timedelta
6+
from io import BytesIO
7+
from pathlib import Path
8+
9+
import requests
10+
from couchbase.n1ql import QueryScanConsistency
11+
from haystack import Pipeline
12+
from haystack.components.converters import TextFileToDocument
13+
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
14+
from haystack.components.preprocessors import DocumentCleaner, DocumentSplitter
15+
from haystack.components.writers import DocumentWriter
16+
from haystack.utils import Secret
17+
18+
from couchbase_haystack import (
19+
CouchbasePasswordAuthenticator,
20+
CouchbaseQueryDocumentStore,
21+
CouchbaseQueryOptions,
22+
QueryVectorSearchType,
23+
)
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
def fetch_archive_from_http(url: str, output_dir: str):
29+
if Path(output_dir).is_dir():
30+
logger.warn(f"'{output_dir}' directory already exists. Skipping data download")
31+
return
32+
33+
with requests.get(url, timeout=10, stream=True) as response:
34+
with zipfile.ZipFile(BytesIO(response.content)) as zip_ref:
35+
zip_ref.extractall(output_dir)
36+
37+
38+
# Let's first get some files that we want to use
39+
docs_dir = "data/docs"
40+
fetch_archive_from_http(
41+
url="https://s3.eu-central-1.amazonaws.com/deepset.ai-farm-qa/datasets/documents/wiki_gameofthrones_txt6.zip",
42+
output_dir=docs_dir,
43+
)
44+
45+
# Make sure you have a running couchbase database, e.g. with Docker:
46+
# docker run \
47+
# --restart always \
48+
# --publish=8091-8096:8091-8096 --publish=11210:11210 \
49+
# --env COUCHBASE_ADMINISTRATOR_USERNAME=admin \
50+
# --env COUCHBASE_ADMINISTRATOR_PASSWORD=passw0rd \
51+
# couchbase:enterprise-8.0.0
52+
53+
bucket_name = "haystack_bucket_name"
54+
scope_name = "haystack_scope_name"
55+
collection_name = "haystack_collection_name"
56+
index_name = "haystack_index_name"
57+
58+
document_store = CouchbaseQueryDocumentStore(
59+
cluster_connection_string=Secret.from_env_var("CONNECTION_STRING"),
60+
authenticator=CouchbasePasswordAuthenticator(username=Secret.from_token("username"), password=Secret.from_token("password")),
61+
bucket=bucket_name,
62+
scope=scope_name,
63+
collection=collection_name,
64+
search_type=QueryVectorSearchType.ANN,
65+
similarity="L2",
66+
nprobes=10,
67+
query_options=CouchbaseQueryOptions(timeout=timedelta(seconds=300), scan_consistency=QueryScanConsistency.REQUEST_PLUS),
68+
)
69+
70+
71+
# Create components and an indexing pipeline that converts txt to documents, cleans and splits them, and
72+
# indexes them for dense retrieval.
73+
p = Pipeline()
74+
p.add_component("text_file_converter", TextFileToDocument())
75+
p.add_component("cleaner", DocumentCleaner())
76+
p.add_component("splitter", DocumentSplitter(split_by="sentence", split_length=250, split_overlap=30))
77+
p.add_component("embedder", SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"))
78+
p.add_component("writer", DocumentWriter(document_store=document_store))
79+
80+
p.connect("text_file_converter.documents", "cleaner.documents")
81+
p.connect("cleaner.documents", "splitter.documents")
82+
p.connect("splitter.documents", "embedder.documents")
83+
p.connect("embedder.documents", "writer.documents")
84+
85+
# Take the docs data directory as input and run the pipeline
86+
file_paths = [docs_dir / Path(name) for name in os.listdir(docs_dir)]
87+
result = p.run({"text_file_converter": {"sources": file_paths}})
88+
89+
# Assuming you have a Docker container running, navigate to <http://localhost:8091>
90+
# to open the Couchbase Web Console and explore your data.
91+
92+
93+
# currently index needs to be created after some documents are available in the collection for training
94+
# this is a current limitation of the couchbase gsi vector index
95+
cfg = {
96+
"dimension": 384,
97+
"train_list": 500,
98+
"description": "IVF,PQ32x8",
99+
"similarity": "L2",
100+
}
101+
document_store.scope.query(f"Create Index {index_name} ON {collection_name} (embedding vector) USING GSI WITH {json.dumps(cfg)}")

examples/gsi/rag_pipeline.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from couchbase.options import KnownConfigProfiles
2+
from haystack import GeneratedAnswer, Pipeline
3+
from haystack.components.builders.answer_builder import AnswerBuilder
4+
from haystack.components.builders.prompt_builder import PromptBuilder
5+
from haystack.components.embedders import SentenceTransformersTextEmbedder
6+
from haystack.components.generators import HuggingFaceAPIGenerator
7+
from haystack.utils import Secret
8+
9+
from couchbase_haystack import (
10+
CouchbaseClusterOptions,
11+
CouchbasePasswordAuthenticator,
12+
CouchbaseQueryDocumentStore,
13+
CouchbaseQueryEmbeddingRetriever,
14+
QueryVectorSearchType,
15+
)
16+
17+
# Load HF Token from environment variables.
18+
HF_TOKEN = Secret.from_env_var("HF_API_TOKEN")
19+
20+
# Make sure you have a running couchbase database, e.g. with Docker:
21+
# docker run \
22+
# --restart always \
23+
# --publish=8091-8096:8091-8096 --publish=11210:11210 \
24+
# --env COUCHBASE_ADMINISTRATOR_USERNAME=admin \
25+
# --env COUCHBASE_ADMINISTRATOR_PASSWORD=passw0rd \
26+
# couchbase:enterprise-8.0.0
27+
28+
document_store = CouchbaseQueryDocumentStore(
29+
cluster_connection_string=Secret.from_token("localhost"),
30+
authenticator=CouchbasePasswordAuthenticator(username=Secret.from_token("username"), password=Secret.from_token("password")),
31+
cluster_options=CouchbaseClusterOptions(
32+
profile=KnownConfigProfiles.WanDevelopment,
33+
),
34+
bucket="haystack_bucket_name",
35+
scope="haystack_scope_name",
36+
collection="haystack_collection_name",
37+
search_type=QueryVectorSearchType.ANN,
38+
similarity="L2",
39+
nprobes=10,
40+
)
41+
42+
# Build a RAG pipeline with a Retriever to get relevant documents to the query and a HuggingFaceTGIGenerator
43+
# interacting with LLMs using a custom prompt.
44+
prompt_template = """
45+
Given these documents, answer the question.\nDocuments:
46+
{% for doc in documents %}
47+
{{ doc.content }}
48+
{% endfor %}
49+
50+
\nQuestion: {{question}}
51+
\nAnswer:
52+
"""
53+
rag_pipeline = Pipeline()
54+
rag_pipeline.add_component(
55+
"query_embedder",
56+
SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2", progress_bar=False),
57+
)
58+
rag_pipeline.add_component("retriever", CouchbaseQueryEmbeddingRetriever(document_store=document_store))
59+
rag_pipeline.add_component("prompt_builder", PromptBuilder(template=prompt_template))
60+
rag_pipeline.add_component(
61+
"llm",
62+
HuggingFaceAPIGenerator(
63+
api_type="serverless_inference_api",
64+
api_params={"model": "mistralai/Mistral-7B-v0.1"},
65+
),
66+
)
67+
rag_pipeline.add_component("answer_builder", AnswerBuilder())
68+
69+
rag_pipeline.connect("query_embedder", "retriever.query_embedding")
70+
rag_pipeline.connect("retriever.documents", "prompt_builder.documents")
71+
rag_pipeline.connect("prompt_builder.prompt", "llm.prompt")
72+
rag_pipeline.connect("llm.replies", "answer_builder.replies")
73+
rag_pipeline.connect("llm.meta", "answer_builder.meta")
74+
rag_pipeline.connect("retriever", "answer_builder.documents")
75+
76+
# Ask a question on the data you just added.
77+
question = "Who created the Dothraki vocabulary?"
78+
result = rag_pipeline.run(
79+
{
80+
"query_embedder": {"text": question},
81+
"retriever": {"top_k": 3},
82+
"prompt_builder": {"question": question},
83+
"answer_builder": {"query": question},
84+
}
85+
)
86+
87+
# For details, like which documents were used to generate the answer, look into the GeneratedAnswer object
88+
answer: GeneratedAnswer = result["answer_builder"]["answers"][0]
89+
90+
# ruff: noqa: T201
91+
print("Query: ", answer.query)
92+
print("Answer: ", answer.data)
93+
print("== Sources:")
94+
for doc in answer.documents:
95+
print("-> ", doc.meta["file_path"])

src/couchbase_haystack/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,29 @@
88
It includes authentication methods, document store implementation, and embedding-based retrieval functionality.
99
"""
1010

11-
from couchbase_haystack.components.retrievers import CouchbaseSearchEmbeddingRetriever
11+
from couchbase_haystack.components.retrievers import CouchbaseQueryEmbeddingRetriever, CouchbaseSearchEmbeddingRetriever
1212
from couchbase_haystack.document_stores import (
1313
CouchbaseAuthenticator,
1414
CouchbaseCertificateAuthenticator,
1515
CouchbaseClusterOptions,
1616
CouchbasePasswordAuthenticator,
17+
CouchbaseQueryDocumentStore,
18+
CouchbaseQueryOptions,
1719
CouchbaseSearchDocumentStore,
20+
QueryVectorSearchSimilarity,
21+
QueryVectorSearchType,
1822
)
1923

2024
__all__ = [
2125
"CouchbaseAuthenticator",
2226
"CouchbaseCertificateAuthenticator",
2327
"CouchbaseClusterOptions",
2428
"CouchbasePasswordAuthenticator",
29+
"CouchbaseQueryDocumentStore",
30+
"CouchbaseQueryEmbeddingRetriever",
31+
"CouchbaseQueryOptions",
2532
"CouchbaseSearchDocumentStore",
2633
"CouchbaseSearchEmbeddingRetriever",
34+
"QueryVectorSearchSimilarity",
35+
"QueryVectorSearchType",
2736
]
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .embedding_retriever import CouchbaseSearchEmbeddingRetriever
1+
from .embedding_retriever import CouchbaseQueryEmbeddingRetriever, CouchbaseSearchEmbeddingRetriever
22

3-
__all__ = ["CouchbaseSearchEmbeddingRetriever"]
3+
__all__ = ["CouchbaseQueryEmbeddingRetriever", "CouchbaseSearchEmbeddingRetriever"]

0 commit comments

Comments
 (0)