Search Strategies
Storage backends expose a
search()search()
SearchStrategySearchStrategyprotocol/interface: given a storage backend and a query
string, it returns a ranked list of matching keys with
relevance scores. Strategies that maintain an index use
index()index()
All shipped backends default to
KeywordSearchStrategy. Pass a different strategy via
the
search_strategysearchStrategy
KeywordSearchStrategy
Section titled “KeywordSearchStrategy”The default strategy shipped with both SDKs. Tokenizes the query into lowercased words, reads every key in storage, and scores each entry by the number of distinct query tokens that appear in the key and content. No index, embedding model, or external dependency required.
from strands.storage import LocalFileStoragefrom strands.storage.search import KeywordSearchStrategy
strategy = KeywordSearchStrategy()storage = LocalFileStorage("./my-data/")results = await strategy.search( storage, "dark mode toggle")import { KeywordSearchStrategy } from '@strands-agents/sdk/storage/search'import { LocalFileStorage } from '@strands-agents/sdk/storage'
const storage = new LocalFileStorage('./my-data/')const results = await KeywordSearchStrategy.search( storage, 'dark mode toggle',)Because it reads every entry on each search, keyword search is best suited for small-to-medium stores (up to a few thousand keys). For larger stores, use the BM25 strategy.
BM25 search
Section titled “BM25 search”BM25 full-text search backed by a SQLite inverted
index. Scores results by term frequency, inverse
document frequency, and document length normalization,
producing significantly better relevance ranking than
keyword overlap for large stores. It works only with
LocalFileStorage, because it keeps its index file on
the host filesystem next to the storage directory. For
the same reason it can’t be used with a sandboxed
LocalFileStorage; use keyword search there, which
works with every backend.
Bm25SearchStrategy uses the built-in sqlite3 FTS5
module, with no external dependencies. It indexes
entries at write time and never re-scans storage, so
only content written after the strategy is attached is
searchable (there is no backfill).
from strands.storage import LocalFileStoragefrom strands.storage.search import Bm25SearchStrategy
strategy = Bm25SearchStrategy()storage = LocalFileStorage( "./memory/", search_strategy=strategy)
await storage.write( "auth.md", b"OAuth2 authentication flow",)results = await storage.search("authentication")
await strategy.close()QmdSearchStrategy uses
QMD and re-indexes the
storage directory on each search, so existing content
is always searchable. It requires @tobilu/qmd as a
peer dependency:
npm install @tobilu/qmdimport { QmdSearchStrategy } from '@strands-agents/sdk/storage/search/qmd'import { LocalFileStorage } from '@strands-agents/sdk/storage'
const search = new QmdSearchStrategy()const storage = new LocalFileStorage('./memory/', { searchStrategy: search,})
await storage.write( 'auth.md', new TextEncoder().encode( 'OAuth2 authentication flow', ),)const results = await storage.search( 'authentication',)
await search.close()The strategy persists a SQLite database alongside the
storage directory. Call
close()close()
Passing a strategy to a storage backend
Section titled “Passing a strategy to a storage backend”Every built-in backend (InMemoryStorage,
LocalFileStorage, S3Storage) accepts a
search_strategysearchStrategyindex()index()search()search()
from strands.storage import LocalFileStoragefrom strands.storage.search import Bm25SearchStrategy
storage = LocalFileStorage( "./memory/", search_strategy=Bm25SearchStrategy(),)import { QmdSearchStrategy } from '@strands-agents/sdk/storage/search/qmd'import { LocalFileStorage } from '@strands-agents/sdk/storage'
const storage = new LocalFileStorage('./memory/', { searchStrategy: new QmdSearchStrategy(),})Writing a custom strategy
Section titled “Writing a custom strategy”To rank results your own way, implement
SearchStrategySearchStrategyyourself. Its search()search()
StorageSearchResultStorageSearchResultentries (key, score, and optionally the stored bytes),
best match first. Read entries through the storage
passed in, using
list()list()read()read()data
so callers don’t have to read the entry again.
If your strategy keeps its own index, such as a vector
store or a full-text index, update it in
index()index()
The example below ranks entries by how often the exact query phrase appears.
SearchStrategy is a protocol, so any class with async
index() and search() methods works without
subclassing. index() is required; a strategy that
searches on the fly implements it as a no-op.
from typing import Any
from strands.storage import InMemoryStorage, Storagefrom strands.storage.search import StorageSearchResult
class PhraseSearchStrategy: """Rank entries by how often the exact query phrase appears."""
async def index( self, storage: Storage, key: str, data: bytes, **kwargs: Any ) -> None: pass # Searches on the fly, so there is nothing to index.
async def search( self, storage: Storage, query: str, **kwargs: Any ) -> list[StorageSearchResult]: phrase = query.strip().lower() if not phrase: return [] results = [] for key in await storage.list(""): data = await storage.read(key) if data is None: continue text = data.decode("utf-8", errors="replace") count = text.lower().count(phrase) if count: results.append( StorageSearchResult(key=key, score=count, data=data) ) results.sort(key=lambda result: result.score, reverse=True) return results
storage = InMemoryStorage(search_strategy=PhraseSearchStrategy())To pass a strategy to a sandboxed LocalFileStorage,
declare requires_host_fs: Literal[False] = False on
the class, as KeywordSearchStrategy does. The type
checker rejects a sandboxed LocalFileStorage with a
strategy that doesn’t declare it. If your strategy
reads or writes the host filesystem directly, declare
requires_host_fs: Literal[True] = True instead, as
Bm25SearchStrategy does, and LocalFileStorage
raises ValueError when a sandbox is set.
Any object or class with a search() method works.
index() is optional; a strategy that searches on the
fly omits it.
import { InMemoryStorage } from '@strands-agents/sdk/storage'import type { Storage } from '@strands-agents/sdk/storage'import type { SearchStrategy, StorageSearchResult,} from '@strands-agents/sdk/storage/search'
// Rank entries by how often the exact query phrase appears.const phraseSearch: SearchStrategy = { async search( storage: Storage, query: string, ): Promise<StorageSearchResult[]> { const phrase = query.trim().toLowerCase() if (!phrase) return [] const results: StorageSearchResult[] = [] for (const key of await storage.list('')) { const data = await storage.read(key) if (!data) continue const text = new TextDecoder().decode(data) const count = text.toLowerCase().split(phrase).length - 1 if (count > 0) results.push({ key, score: count, data }) } return results.sort((a, b) => b.score - a.score) },}
const storage = new InMemoryStorage({ searchStrategy: phraseSearch,})If your strategy reads or writes the host filesystem
directly instead of going through the storage, declare
readonly requiresHostFs = true on it.
LocalFileStorage then throws when a sandbox is set,
as it does for QmdSearchStrategy.