In this guide, we will walk you through building a powerful semantic search engine using Couchbase as the backend database, OpenAI as the embedding and LLM provider, and Hugging Face smolagents as an agent framework. Semantic search goes beyond simple keyword matching by understanding the context and meaning behind the words in a query, making it an essential tool for applications that require intelligent information retrieval. This tutorial is designed to be beginner-friendly, with clear, step-by-step instructions that will equip you with the knowledge to create a fully functional semantic search system using Couchbase's Hyperscale and Composite Vector Indexes from scratch. Alternatively if you want to perform semantic search using the Search Vector Index, please take a look at this.
This tutorial is available as a Jupyter Notebook (.ipynb file) that you can run interactively. You can access the original notebook here.
You can either download the notebook file and run it on Google Colab or run it on your system by setting up the Python environment.
Please follow the instructions to generate the OpenAI credentials.
To get started with Couchbase Capella, create an account and use it to deploy a forever free tier operational cluster. This account provides you with an environment where you can explore and learn about Capella with no time constraint.
To learn more, please follow the instructions.
Note: To run this this tutorial, you will need Capella with Couchbase Server version 8.0 or above as Couchbase Hyperscale and Composite Vector Index is supported only from version 8.0
When running Couchbase using Capella, the following prerequisites need to be met.
To build our semantic search engine, we need a robust set of tools. The libraries we install handle everything from connecting to databases to performing complex machine learning tasks. Each library has a specific role: Couchbase libraries manage database operations, LangChain handles AI model integrations, and OpenAI provides advanced AI models for generating embeddings and understanding natural language. By setting up these libraries, we ensure our environment is equipped to handle the data-intensive and computationally complex tasks required for semantic search.
%pip install --quiet datasets==4.1.1 langchain-couchbase==0.5.0 langchain-openai==0.3.33 python-dotenv==1.1.1 smolagents==1.21.3
The script starts by importing a series of libraries required for various tasks, including handling JSON, logging, time tracking, Couchbase connections, embedding generation, and dataset loading. These libraries provide essential functions for working with data, managing database connections, and processing machine learning models.
import getpass
import json
import logging
import os
import time
from datetime import timedelta
from couchbase.auth import PasswordAuthenticator
from couchbase.cluster import Cluster
from couchbase.exceptions import (CouchbaseException,
InternalServerFailureException,
QueryIndexAlreadyExistsException)
from couchbase.management.buckets import CreateBucketSettings
from couchbase.options import ClusterOptions
from datasets import load_dataset
from dotenv import load_dotenv
from langchain_couchbase.vectorstores import CouchbaseQueryVectorStore
from langchain_couchbase.vectorstores import DistanceStrategy
from langchain_couchbase.vectorstores import IndexType
from langchain_openai import OpenAIEmbeddings
from smolagents import Tool, OpenAIServerModel, ToolCallingAgent
Logging is configured to track the progress of the script and capture any errors or warnings. This is crucial for debugging and understanding the flow of execution. The logging output includes timestamps, log levels (e.g., INFO, ERROR), and messages that describe what is happening in the script.
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', force=True)
# Disable all logging except critical to prevent OpenAI API request logs
logging.getLogger("httpx").setLevel(logging.CRITICAL)
In this section, we prompt the user to input essential configuration settings needed. These settings include sensitive information like API keys, database credentials, and specific configuration names. Instead of hardcoding these details into the script, we request the user to provide them at runtime, ensuring flexibility and security.
The script also validates that all required inputs are provided, raising an error if any crucial information is missing. This approach ensures that your integration is both secure and correctly configured without hardcoding sensitive information, enhancing the overall security and maintainability of your code.
load_dotenv()
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') or getpass.getpass('Enter your OpenAI API Key: ')
CB_HOST = os.getenv('CB_HOST') or input('Enter your Couchbase host (default: couchbase://localhost): ') or 'couchbase://localhost'
CB_USERNAME = os.getenv('CB_USERNAME') or input('Enter your Couchbase username (default: Administrator): ') or 'Administrator'
CB_PASSWORD = os.getenv('CB_PASSWORD') or getpass.getpass('Enter your Couchbase password (default: password): ') or 'password'
CB_BUCKET_NAME = os.getenv('CB_BUCKET_NAME') or input('Enter your Couchbase bucket name (default: query-vector-search-testing): ') or 'query-vector-search-testing'
SCOPE_NAME = os.getenv('SCOPE_NAME') or input('Enter your scope name (default: shared): ') or 'shared'
COLLECTION_NAME = os.getenv('COLLECTION_NAME') or input('Enter your collection name (default: smolagents): ') or 'smolagents'
# Check if the variables are correctly loaded
if not OPENAI_API_KEY:
raise ValueError("Missing OpenAI API Key")
if 'OPENAI_API_KEY' not in os.environ:
os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY
Connecting to a Couchbase cluster is the foundation of our project. Couchbase will serve as our primary data store, handling all the storage and retrieval operations required for our semantic search engine. By establishing this connection, we enable our application to interact with the database, allowing us to perform operations such as storing embeddings, querying data, and managing collections. This connection is the gateway through which all data will flow, so ensuring it's set up correctly is paramount.
try:
auth = PasswordAuthenticator(CB_USERNAME, CB_PASSWORD)
options = ClusterOptions(auth)
cluster = Cluster(CB_HOST, options)
cluster.wait_until_ready(timedelta(seconds=5))
logging.info("Successfully connected to Couchbase")
except Exception as e:
raise ConnectionError(f"Failed to connect to Couchbase: {str(e)}")
2025-11-07 16:44:51,506 - INFO - Successfully connected to CouchbaseThe setup_collection() function handles creating and configuring the hierarchical data organization in Couchbase:
Bucket Creation:
Scope Management:
Collection Setup:
Additional Tasks:
def setup_collection(cluster, bucket_name, scope_name, collection_name):
try:
# Check if bucket exists, create if it doesn't
try:
bucket = cluster.bucket(bucket_name)
logging.info(f"Bucket '{bucket_name}' exists.")
except Exception as e:
logging.info(f"Bucket '{bucket_name}' does not exist. Creating it...")
bucket_settings = CreateBucketSettings(
name=bucket_name,
bucket_type='couchbase',
ram_quota_mb=1024,
flush_enabled=True,
num_replicas=0
)
cluster.buckets().create_bucket(bucket_settings)
time.sleep(2) # Wait for bucket creation to complete and become available
bucket = cluster.bucket(bucket_name)
logging.info(f"Bucket '{bucket_name}' created successfully.")
bucket_manager = bucket.collections()
# Check if scope exists, create if it doesn't
scopes = bucket_manager.get_all_scopes()
scope_exists = any(scope.name == scope_name for scope in scopes)
if not scope_exists and scope_name != "_default":
logging.info(f"Scope '{scope_name}' does not exist. Creating it...")
bucket_manager.create_scope(scope_name)
logging.info(f"Scope '{scope_name}' created successfully.")
# Check if collection exists, create if it doesn't
collections = bucket_manager.get_all_scopes()
collection_exists = any(
scope.name == scope_name and collection_name in [col.name for col in scope.collections]
for scope in collections
)
if not collection_exists:
logging.info(f"Collection '{collection_name}' does not exist. Creating it...")
bucket_manager.create_collection(scope_name, collection_name)
logging.info(f"Collection '{collection_name}' created successfully.")
else:
logging.info(f"Collection '{collection_name}' already exists. Skipping creation.")
# Wait for collection to be ready
collection = bucket.scope(scope_name).collection(collection_name)
time.sleep(2) # Give the collection time to be ready for queries
# Clear all documents in the collection
try:
query = f"DELETE FROM `{bucket_name}`.`{scope_name}`.`{collection_name}`"
cluster.query(query).execute()
logging.info("All documents cleared from the collection.")
except Exception as e:
logging.warning(f"Error while clearing documents: {str(e)}. The collection might be empty.")
return collection
except Exception as e:
raise RuntimeError(f"Error setting up collection: {str(e)}")
setup_collection(cluster, CB_BUCKET_NAME, SCOPE_NAME, COLLECTION_NAME)
2025-11-07 16:44:53,519 - INFO - Bucket 'travel-sample' exists.
2025-11-07 16:44:53,527 - INFO - Collection 'smolagents' does not exist. Creating it...
2025-11-07 16:44:53,575 - INFO - Collection 'smolagents' created successfully.
2025-11-07 16:44:55,731 - INFO - All documents cleared from the collection.
<couchbase.collection.Collection at 0x17bdb9be0>Embeddings are at the heart of semantic search. They are numerical representations of text that capture the semantic meaning of the words and phrases. Unlike traditional keyword-based search, which looks for exact matches, embeddings allow our search engine to understand the context and nuances of language, enabling it to retrieve documents that are semantically similar to the query, even if they don't contain the exact keywords. By creating embeddings using OpenAI, we equip our search engine with the ability to understand and process natural language in a way that's much closer to how humans understand language. This step transforms our raw text data into a format that the search engine can use to find and rank relevant documents.
try:
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key=OPENAI_API_KEY,
)
logging.info("Successfully created OpenAIEmbeddings")
except Exception as e:
raise ValueError(f"Error creating OpenAIEmbeddings: {str(e)}")
2025-11-07 16:44:58,634 - INFO - Successfully created OpenAIEmbeddingsWith Couchbase 8.0+, you can leverage the power of Hyperscale and Composite Vector Index-based vector search, which offers significant performance improvements over traditional Full-Text Search (FTS) approaches for vector-first workloads. Hyperscale and Composite Vector Index provides high-performance vector similarity search with advanced filtering capabilities and is designed to scale to billions of vectors.
| Feature | Hyperscale and Composite Vector Index | Search Vector Index |
|---|---|---|
| Best For | Vector-first workloads, complex filtering, high QPS performance | Hybrid search and high recall rates |
| Couchbase Version | 8.0.0+ | 7.6+ |
| Filtering | Pre-filtering with WHERE clauses (Composite) or post-filtering |
Pre-filtering with flexible ordering |
| Scalability | Up to billions of vectors | Up to 10 million vectors |
| Performance | Optimized for concurrent operations with low memory footprint | Good for mixed text and vector queries |
Couchbase offers two distinct vector indexes, Hyperscale and Composite, each optimized for different use cases:
In this tutorial, we'll demonstrate creating a Hyperscale index and running vector similarity queries using Hyperscale and Composite Vector Index. Hyperscale index is ideal for semantic search scenarios where you want:
The Hyperscale index will provide optimal performance for our OpenAI embedding-based semantic search implementation.
If your use case requires complex filtering with scalar attributes, you may want to consider using a Composite Vector Index instead:
# Alternative: Create a Composite index for filtered searches
vector_store.create_index(
index_type=IndexType.COMPOSITE,
index_description="IVF,SQ8",
distance_metric=DistanceStrategy.COSINE,
index_name="smolagents_composite_index",
)Use Composite indexes when:
Note: Composite indexes enable pre-filtering with scalar attributes, making them ideal for applications where you need to search within specific categories, date ranges, or user-specific data segments.
Before creating our Hyperscale index, it's important to understand the configuration parameters that optimize vector storage and search performance. The index_description parameter controls how Couchbase optimizes vector storage through centroids and quantization.
'IVF[<centroids>],{PQ|SQ}<settings>'IVF,SQ8), Couchbase auto-selects based on dataset sizeScalar Quantization (SQ):
SQ4, SQ6, SQ8 (4, 6, or 8 bits per dimension)Product Quantization (PQ):
PQ<subquantizers>x<bits> (e.g., PQ32x8)IVF,SQ8 - Auto centroids, 8-bit scalar quantization (good default)IVF1000,SQ6 - 1000 centroids, 6-bit scalar quantizationIVF,PQ32x8 - Auto centroids, 32 subquantizers with 8 bitsFor detailed configuration options, see the Quantization & Centroid Settings.
For more information on Hyperscale and Composite Vector Index, see Couchbase Hyperscale and Composite Vector Index Documentation.
In this tutorial, we use IVF,SQ8 which provides:
A vector store is where we'll keep our embeddings. The query vector store is specifically designed to handle embeddings and perform similarity searches. When a user inputs a query, Hyperscale index converts the query into an embedding and compares it against the embeddings stored in the vector store. This allows the engine to find documents that are semantically similar to the query, even if they don't contain the exact same words. By setting up the vector store in Couchbase, we create a powerful tool that enables us to understand and retrieve information based on the meaning and context of the query, rather than just the specific words used.
The vector store requires a distance metric to determine how similarity between vectors is calculated. This is crucial for accurate semantic search results as different distance metrics can yield different similarity rankings. Some of the supported Distance strategies are dot, l2, euclidean, cosine, l2_squared, euclidean_squared. In our implementation we will use cosine which is particularly effective for text embeddings.
try:
vector_store = CouchbaseQueryVectorStore(
cluster=cluster,
bucket_name=CB_BUCKET_NAME,
scope_name=SCOPE_NAME,
collection_name=COLLECTION_NAME,
embedding=embeddings,
distance_metric=DistanceStrategy.COSINE
)
logging.info("Successfully created vector store")
except Exception as e:
raise ValueError(f"Failed to create vector store: {str(e)}")
To build a search engine, we need data to search through. We use the BBC News dataset from RealTimeData, which provides real-world news articles. This dataset contains news articles from BBC covering various topics and time periods. Loading the dataset is a crucial step because it provides the raw material that our search engine will work with. The quality and diversity of the news articles make it an excellent choice for testing and refining our search engine, ensuring it can handle real-world news content effectively.
The BBC News dataset allows us to work with authentic news articles, enabling us to build and test a search engine that can effectively process and retrieve relevant news content. The dataset is loaded using the Hugging Face datasets library, specifically accessing the "RealTimeData/bbc_news_alltime" dataset with the "2024-12" version.
try:
news_dataset = load_dataset(
"RealTimeData/bbc_news_alltime", "2024-12", split="train"
)
print(f"Loaded the BBC News dataset with {len(news_dataset)} rows")
logging.info(f"Successfully loaded the BBC News dataset with {len(news_dataset)} rows.")
except Exception as e:
raise ValueError(f"Error loading the BBC News dataset: {str(e)}")
We will use the content of the news articles for our RAG system.
The dataset contains a few duplicate records. We are removing them to avoid duplicate results in the retrieval stage of our RAG system.
news_articles = news_dataset["content"]
unique_articles = set()
for article in news_articles:
if article:
unique_articles.add(article)
unique_news_articles = list(unique_articles)
print(f"We have {len(unique_news_articles)} unique articles in our database.")
We have 1749 unique articles in our database.To efficiently handle the large number of articles, we process them in batches of articles at a time. This batch processing approach helps manage memory usage and provides better control over the ingestion process.
We first filter out any articles that exceed 50,000 characters to avoid potential issues with token limits. Then, using the vector store's add_texts method, we add the filtered articles to our vector database. The batch_size parameter controls how many articles are processed in each iteration.
This approach offers several benefits:
We use a conservative batch size of 100 to ensure reliable operation. The optimal batch size depends on many factors including:
Consider measuring performance with your specific workload before adjusting.
batch_size = 100
articles = [article for article in unique_news_articles if article and len(article) <= 50000]
try:
vector_store.add_texts(
texts=articles,
batch_size=batch_size
)
logging.info("Document ingestion completed successfully.")
except Exception as e:
raise ValueError(f"Failed to save documents to vector store: {str(e)}")
2025-11-07 16:46:18,967 - INFO - Document ingestion completed successfully.Semantic search in Couchbase involves converting queries and documents into vector representations using an embeddings model. These vectors capture the semantic meaning of the text and are stored directly in Couchbase. When a query is made, Couchbase performs a similarity search by comparing the query vector against the stored document vectors. The similarity metric used for this comparison is configurable, allowing flexibility in how the relevance of documents is determined. Common metrics include cosine similarity, Euclidean distance, or dot product, but other metrics can be implemented based on specific use cases. Different embedding models like BERT, Word2Vec, or GloVe can also be used depending on the application's needs, with the vectors generated by these models stored and searched within Couchbase itself.
In the provided code, the search process begins by recording the start time, followed by executing the similarity_search_with_score method of the CouchbaseQueryVectorStore. This method searches Couchbase for the most relevant documents based on the vector similarity to the query. The search results include the document content and the distance that reflects how closely each document aligns with the query in the defined semantic space. The time taken to perform this search is then calculated and logged, and the results are displayed, showing the most relevant documents along with their similarity scores. This approach leverages Couchbase as both a storage and retrieval engine for vector data, enabling efficient and scalable semantic searches. The integration of vector storage and search capabilities within Couchbase allows for sophisticated semantic search operations without relying on external services for vector storage or comparison.
Now let's measure and compare the performance benefits of different optimization strategies. We'll conduct a comprehensive performance analysis across two phases:
Important Context:
# Phase 1: Baseline Performance (Without Hyperscale vector index)
print("="*80)
print("PHASE 1: BASELINE PERFORMANCE (NO Hyperscale vector index)")
print("="*80)
query = "What was manchester city manager pep guardiola's reaction to the team's current form?"
try:
# Perform the semantic search
start_time = time.time()
search_results = vector_store.similarity_search_with_score(query, k=10)
baseline_time = time.time() - start_time
logging.info(f"Semantic search completed in {baseline_time:.2f} seconds")
# Display search results
print(f"\nSemantic Search Results (completed in {baseline_time:.2f} seconds):")
print("-" * 80) # Add separator line
for doc, distance in search_results:
print(f"Vector Distance: {distance:.4f}, Text: {doc.page_content}")
print("-" * 80) # Add separator between results
except CouchbaseException as e:
raise RuntimeError(f"Error performing semantic search: {str(e)}")
except Exception as e:
raise RuntimeError(f"Unexpected error: {str(e)}")
================================================================================
PHASE 1: BASELINE PERFORMANCE
================================================================================
2025-11-07 16:46:24,561 - INFO - Semantic search completed in 1.34 seconds
Semantic Search Results (completed in 1.34 seconds):
--------------------------------------------------------------------------------
Vector Distance: 0.2956, Text: Manchester City boss Pep Guardiola has won 18 trophies since he arrived at the club in 2016
Manchester City boss Pep Guardiola says he is "fine" despite admitting his sleep and diet are being affected by the worst run of results in his entire managerial career. In an interview with former Italy international Luca Toni for Amazon Prime Sport before Wednesday's Champions League defeat by Juventus, Guardiola touched on the personal impact City's sudden downturn in form has had. Guardiola said his state of mind was "ugly", that his sleep was "worse" and he was eating lighter as his digestion had suffered. City go into Sunday's derby against Manchester United at Etihad Stadium having won just one of their past 10 games. The Juventus loss means there is a chance they may not even secure a play-off spot in the Champions League. Asked to elaborate on his comments to Toni, Guardiola said: "I'm fine. "In our jobs we always want to do our best or the best as possible. When that doesn't happen you are more uncomfortable than when the situation is going well, always that happened. "In good moments I am happier but when I get to the next game I am still concerned about what I have to do. There is no human being that makes an activity and it doesn't matter how they do." Guardiola said City have to defend better and "avoid making mistakes at both ends". To emphasise his point, Guardiola referred back to the third game of City's current run, against a Sporting side managed by Ruben Amorim, who will be in the United dugout at the weekend. City dominated the first half in Lisbon, led thanks to Phil Foden's early effort and looked to be cruising. Instead, they conceded three times in 11 minutes either side of half-time as Sporting eventually ran out 4-1 winners. "I would like to play the game like we played in Lisbon on Sunday, believe me," said Guardiola, who is facing the prospect of only having three fit defenders for the derby as Nathan Ake and Manuel Akanji try to overcome injury concerns. If there is solace for City, it comes from the knowledge United are not exactly flying. Their comeback Europa League victory against Viktoria Plzen on Thursday was their third win of Amorim's short reign so far but only one of those successes has come in the Premier League, where United have lost their past two games against Arsenal and Nottingham Forest. Nevertheless, Guardiola can see improvements already on the red side of the city. "It's already there," he said. "You see all the patterns, the movements, the runners and the pace. He will do a good job at United, I'm pretty sure of that."
Guardiola says skipper Kyle Walker has been offered support by the club after the City defender highlighted the racial abuse he had received on social media in the wake of the Juventus trip. "It's unacceptable," he said. "Not because it's Kyle - for any human being. "Unfortunately it happens many times in the real world. It is not necessary to say he has the support of the entire club. It is completely unacceptable and we give our support to him."
--------------------------------------------------------------------------------
Vector Distance: 0.3100, Text: Pep Guardiola has said Manchester City will be his final managerial job in club football before he "maybe" coaches a national team.
--------------------------------------------------------------------------------vector_store.create_index(index_type=IndexType.BHIVE, index_name="smolagents_bhive_index", index_description="IVF,SQ8")# Phase 2: Hyperscale-Optimized Performance
print("\n" + "="*80)
print("PHASE 2: HYPERSCALE-OPTIMIZED PERFORMANCE")
print("="*80)
query = "What was manchester city manager pep guardiola's reaction to the team's current form?"
try:
# Perform the semantic search
start_time = time.time()
search_results = vector_store.similarity_search_with_score(query, k=10)
hyperscale_time = time.time() - start_time
logging.info(f"Semantic search completed in {hyperscale_time:.2f} seconds")
# Display search results
print(f"\nSemantic Search Results (completed in {hyperscale_time:.2f} seconds):")
print("-" * 80) # Add separator line
for doc, distance in search_results:
print(f"Vector Distance: {distance:.4f}, Text: {doc.page_content}")
print("-" * 80) # Add separator between results
except CouchbaseException as e:
raise RuntimeError(f"Error performing semantic search: {str(e)}")
except Exception as e:
raise RuntimeError(f"Unexpected error: {str(e)}")
================================================================================
PHASE 2: HYPERSCALE-OPTIMIZED PERFORMANCE
================================================================================
2025-11-07 16:47:01,538 - INFO - Semantic search completed in 0.42 seconds
Semantic Search Results (completed in 0.42 seconds):
--------------------------------------------------------------------------------
Vector Distance: 0.2956, Text: Manchester City boss Pep Guardiola has won 18 trophies since he arrived at the club in 2016
Manchester City boss Pep Guardiola says he is "fine" despite admitting his sleep and diet are being affected by the worst run of results in his entire managerial career. In an interview with former Italy international Luca Toni for Amazon Prime Sport before Wednesday's Champions League defeat by Juventus, Guardiola touched on the personal impact City's sudden downturn in form has had. Guardiola said his state of mind was "ugly", that his sleep was "worse" and he was eating lighter as his digestion had suffered. City go into Sunday's derby against Manchester United at Etihad Stadium having won just one of their past 10 games. The Juventus loss means there is a chance they may not even secure a play-off spot in the Champions League. Asked to elaborate on his comments to Toni, Guardiola said: "I'm fine. "In our jobs we always want to do our best or the best as possible. When that doesn't happen you are more uncomfortable than when the situation is going well, always that happened. "In good moments I am happier but when I get to the next game I am still concerned about what I have to do. There is no human being that makes an activity and it doesn't matter how they do." Guardiola said City have to defend better and "avoid making mistakes at both ends". To emphasise his point, Guardiola referred back to the third game of City's current run, against a Sporting side managed by Ruben Amorim, who will be in the United dugout at the weekend. City dominated the first half in Lisbon, led thanks to Phil Foden's early effort and looked to be cruising. Instead, they conceded three times in 11 minutes either side of half-time as Sporting eventually ran out 4-1 winners. "I would like to play the game like we played in Lisbon on Sunday, believe me," said Guardiola, who is facing the prospect of only having three fit defenders for the derby as Nathan Ake and Manuel Akanji try to overcome injury concerns. If there is solace for City, it comes from the knowledge United are not exactly flying. Their comeback Europa League victory against Viktoria Plzen on Thursday was their third win of Amorim's short reign so far but only one of those successes has come in the Premier League, where United have lost their past two games against Arsenal and Nottingham Forest. Nevertheless, Guardiola can see improvements already on the red side of the city. "It's already there," he said. "You see all the patterns, the movements, the runners and the pace. He will do a good job at United, I'm pretty sure of that."
Guardiola says skipper Kyle Walker has been offered support by the club after the City defender highlighted the racial abuse he had received on social media in the wake of the Juventus trip. "It's unacceptable," he said. "Not because it's Kyle - for any human being. "Unfortunately it happens many times in the real world. It is not necessary to say he has the support of the entire club. It is completely unacceptable and we give our support to him."
--------------------------------------------------------------------------------
Vector Distance: 0.3100, Text: Pep Guardiola has said Manchester City will be his final managerial job in club football before he "maybe" coaches a national team.
--------------------------------------------------------------------------------Let's analyze the performance improvements we've achieved through different optimization strategies:
print("\n" + "="*80)
print("VECTOR SEARCH PERFORMANCE OPTIMIZATION SUMMARY")
print("="*80)
print(f"\nš Performance Comparison:")
print(f"{'Optimization Level':<35} {'Time (seconds)':<20} {'Status'}")
print("-" * 80)
print(f"{'Phase 1 - Baseline (No Hyperscale)':<35} {baseline_time:.4f}{'':16} āŖ Baseline")
print(f"{'Phase 2 - Hyperscale-Optimized Search':<35} {hyperscale_time:.4f}{'':16} ā
Optimized")
# Calculate improvement
if baseline_time > hyperscale_time:
speedup = baseline_time / hyperscale_time
improvement = ((baseline_time - hyperscale_time) / baseline_time) * 100
print(f"\n⨠Hyperscale Index Performance Gain: {speedup:.2f}x faster ({improvement:.1f}% improvement)")
elif gsi_time > baseline_time:
slowdown_pct = ((gsi_time - baseline_time) / baseline_time) * 100
print(f"\nā ļø Note: Hyperscale Index was {slowdown_pct:.1f}% slower than baseline in this run")
print(f" This can happen with small datasets. Hyperscale Index benefits emerge with scale.")
else:
print(f"\nāļø Performance: Comparable to baseline")
print("\n" + "-"*80)
print("KEY INSIGHTS:")
print("-"*80)
print("1. š Hyperscale Index Optimization:")
print(" ⢠Hyperscale indexes excel with large-scale datasets (millions+ vectors)")
print(" ⢠Performance gains increase with dataset size and concurrent queries")
print(" ⢠Optimal for production workloads with sustained traffic patterns")
print("\n2. š¦ Dataset Size Impact:")
print(f" ⢠Current dataset: ~1,700 articles")
print(" ⢠At this scale, performance differences may be minimal or variable")
print(" ⢠Significant gains typically seen with 10M+ vectors")
print("\n3. šÆ When to Use Hyperscale Index:")
print(" ⢠Large-scale vector search applications")
print(" ⢠High query-per-second (QPS) requirements")
print(" ⢠Multi-user concurrent access scenarios")
print(" ⢠Production environments requiring scalability")
print("\n" + "="*80)
================================================================================
VECTOR SEARCH PERFORMANCE OPTIMIZATION SUMMARY
================================================================================
š Performance Comparison:
Optimization Level Time (seconds) Status
--------------------------------------------------------------------------------
Phase 1 - Baseline (No Index) 1.3410 āŖ Baseline
Phase 2 - Hyperscale-Optimized 0.4157 ā
Optimized
⨠Hyperscale Index Performance Gain: 3.23x faster (69.0% improvement)
--------------------------------------------------------------------------------
KEY INSIGHTS:
--------------------------------------------------------------------------------
1. š Hperscale Index Optimization:
⢠Hyperscale indexes excel with large-scale datasets (millions+ vectors)
⢠Performance gains increase with dataset size and concurrent queries
⢠Optimal for production workloads with sustained traffic patterns
2. š¦ Dataset Size Impact:
⢠Current dataset: ~1,700 articles
⢠At this scale, performance differences may be minimal or variable
⢠Significant gains typically seen with 10M+ vectors
3. šÆ When to Use Hyperscale Indexes:
⢠Large-scale vector search applications
⢠High query-per-second (QPS) requirements
⢠Multi-user concurrent access scenarios
⢠Production environments requiring scalability
================================================================================smolagents is a agentic framework by Hugging Face for easy creation of agents in a few lines of code.
Some of the features of smolagents are:
⨠Simplicity: the logic for agents fits in ~1,000 lines of code (see agents.py). We kept abstractions to their minimal shape above raw code!
š§āš» First-class support for Code Agents. Our CodeAgent writes its actions in code (as opposed to "agents being used to write code"). To make it secure, we support executing in sandboxed environments via E2B.
š¤ Hub integrations: you can share/pull tools to/from the Hub, and more is to come!
š Model-agnostic: smolagents supports any LLM. It can be a local transformers or ollama model, one of many providers on the Hub, or any model from OpenAI, Anthropic and many others via our LiteLLM integration.
šļø Modality-agnostic: Agents support text, vision, video, even audio inputs! Cf this tutorial for vision.
š ļø Tool-agnostic: you can use tools from LangChain, Anthropic's MCP, you can even use a Hub Space as a tool.
smolagents allows users to define their own tools for the agent to use. These tools can be of two types:
Tool class and must override the forward method, which is called when the tool is used.@tool decorator.In our case, we will use the first method, and we define our RetrieverTool below. We define a name, a description and a dictionary of inputs that the tool accepts. This helps the LLM properly identify and use the tool.
The RetrieverTool is simple: it takes a query generated by the user, and uses Couchbase's performant vector search service under the hood to search for semantically similar documents to the query. The LLM can then use this context to answer the user's question.
class RetrieverTool(Tool):
name = "retriever"
description = "Uses semantic search to retrieve the parts of news documentation that could be most relevant to answer your query."
inputs = {
"query": {
"type": "string",
"description": "The query to perform. This should be semantically close to your target documents. Use the affirmative form rather than a question.",
}
}
output_type = "string"
def __init__(self, vector_store: CouchbaseQueryVectorStore, **kwargs):
super().__init__(**kwargs)
self.vector_store = vector_store
def forward(self, query: str) -> str:
assert isinstance(query, str), "Query must be a string"
docs = self.vector_store.similarity_search_with_score(query, k=5)
return "\n\n".join(
f"# Documents:\n{doc.page_content}"
for doc, distance in docs
)
retriever_tool = RetrieverTool(vector_store)
smolagents have predefined configurations for agents that we can use. We use the ToolCallingAgent, which writes its tool calls in a JSON format. Alternatively, there also exists a CodeAgent, in which the LLM defines it's functions in code.
The CodeAgent is offers benefits in certain challenging scenarios: it can lead to higher performance in difficult benchmarks and use 30% fewer steps to solve problems. However, since our use case is just a simple RAG tool, a ToolCallingAgent will suffice.
agent = ToolCallingAgent(
tools=[retriever_tool],
model=OpenAIServerModel(
model_id="gpt-4o-2024-08-06",
api_key=OPENAI_API_KEY,
),
max_steps=4,
verbosity_level=2
)
We have now finished setting up our vector store and agent! The system is now ready to accept queries.
query = "What was manchester city manager pep guardiola's reaction to the team's current form?"
agent_output = agent.run(query)When the agent runs, smolagents prints out the steps that the agent takes along with the tools called in each step. In the above tool call, two steps occur:
Step 1: First, the agent determines that it requires a tool to be used, and the retriever tool is called. The agent also specifies the query parameter for the tool (a string). The tool returns semantically similar documents to the query from Couchbase's vector store.
Step 2: Next, the agent determines that the context retrieved from the tool is sufficient to answer the question. It then calls the final_answer tool, which is predefined for each agent: this tool is called when the agent returns the final answer to the user. In this step, the LLM answers the user's query from the context retrieved in step 1 and passes it to the final_answer tool, at which point the agent's execution ends.
By following these steps, you'll have a fully functional agentic RAG system that leverages the strengths of Couchbase and smolagents, along with OpenAI. This guide is designed not just to show you how to build the system, but also to explain why each step is necessary, giving you a deeper understanding of the principles behind semantic search and how to implement it effectively using Hyperscale and Composite Vector Index which can significantly improve your RAG performance. Whether you're a newcomer to software development or an experienced developer looking to expand your skills, this guide will provide you with the knowledge and tools you need to create a powerful, RAG-driven chat system using smolagents' agent framework.