Weaviate is an open-source vector database with hybrid search — await weaviate.connectToLocal() or weaviate.connectToWeaviateCloud({ connectionParams: { host: url, grpcHost }, auth: new ApiKey(key) }) creates the client. client.collections.create({ name: "Article", vectorizers: weaviate.configure.vectorizer.text2VecOpenAI(), properties: [{ name: "title", dataType: dataType.TEXT }] }) defines a collection. collection.data.insert({ title, body, source }) inserts with auto-vectorization. collection.query.nearText(["machine learning"], { limit: 10, returnMetadata: ["score"], filters: { ... } }) does semantic search. Hybrid: collection.query.hybrid("AI embeddings", { alpha: 0.75, limit: 10 }) blends BM25 and vector results. Multi-tenancy: collection.withTenant("tenant-id").data.insert(...). Cross-references: properties: [{ name: "author", dataType: dataType.OBJECT, nestedProperties: [...] }]. Generative: collection.generate.nearText(["summary"], { singlePrompt: "Summarize: {body}" }). collection.data.insertMany(objects) bulk inserts. Claude Code generates Weaviate RAG systems, hybrid search APIs, and generative document search.
CLAUDE.md for Weaviate
## Weaviate Stack
- Version: weaviate-client >= 3.3 (v3 TypeScript client)
- Local: const client = await weaviate.connectToLocal({ httpHost: "localhost", httpPort: 8080, grpcHost: "localhost", grpcPort: 50051 })
- Cloud: const client = await weaviate.connectToWeaviateCloud({ connectionParams: { host: process.env.WCD_URL!, grpcHost: process.env.WCD_GRPC_URL! }, auth: new weaviate.ApiKey(process.env.WCD_API_KEY!) })
- Collection: const col = client.collections.get("Article")
- Insert: await col.data.insert({ title, body }) — auto-vectorized by configured vectorizer
- Search: const result = await col.query.nearText(["query"], { limit: 5, returnMetadata: ["score"] })
- Hybrid: await col.query.hybrid("query", { alpha: 0.7, limit: 8 }) — alpha=1 is pure vector, alpha=0 is pure BM25
Weaviate Client Setup
// lib/weaviate/client.ts — Weaviate v3 TypeScript client
import weaviate, { type WeaviateClient } from "weaviate-client"
let _client: WeaviateClient | null = null
export async function getWeaviateClient(): Promise<WeaviateClient> {
if (_client) return _client
const isLocal = process.env.NODE_ENV === "development" && !process.env.WCD_URL
if (isLocal) {
_client = await weaviate.connectToLocal({
httpHost: process.env.WEAVIATE_HOST ?? "localhost",
httpPort: parseInt(process.env.WEAVIATE_PORT ?? "8080"),
grpcHost: process.env.WEAVIATE_GRPC_HOST ?? "localhost",
grpcPort: parseInt(process.env.WEAVIATE_GRPC_PORT ?? "50051"),
})
} else {
_client = await weaviate.connectToWeaviateCloud({
connectionParams: {
host: process.env.WCD_URL!,
grpcHost: process.env.WCD_GRPC_URL!,
},
auth: new weaviate.ApiKey(process.env.WCD_API_KEY!),
})
}
return _client
}
Schema and Collection Management
// lib/weaviate/schema.ts — collection creation and management
import weaviate, { type WeaviateClient, dataType, vectorizer, configure } from "weaviate-client"
export const COLLECTIONS = {
DOCUMENTS: "Document",
CHUNKS: "Chunk",
} as const
export async function ensureSchema(client: WeaviateClient) {
const existing = await client.collections.listAll()
const existingNames = new Set(existing.map((c) => c.name))
if (!existingNames.has(COLLECTIONS.DOCUMENTS)) {
await client.collections.create({
name: COLLECTIONS.DOCUMENTS,
description: "Top-level document metadata",
properties: [
{ name: "docId", dataType: dataType.TEXT },
{ name: "title", dataType: dataType.TEXT },
{ name: "source", dataType: dataType.TEXT },
{ name: "category", dataType: dataType.TEXT },
{ name: "userId", dataType: dataType.TEXT },
{ name: "createdAt", dataType: dataType.DATE },
],
// Documents don't need their own vector — use Chunk for search
vectorizers: weaviate.configure.vectorizer.none(),
})
}
if (!existingNames.has(COLLECTIONS.CHUNKS)) {
await client.collections.create({
name: COLLECTIONS.CHUNKS,
description: "Text chunks for semantic search",
properties: [
{ name: "docId", dataType: dataType.TEXT },
{ name: "chunkIndex", dataType: dataType.INT },
{ name: "text", dataType: dataType.TEXT },
{ name: "title", dataType: dataType.TEXT },
{ name: "source", dataType: dataType.TEXT },
{ name: "category", dataType: dataType.TEXT },
{ name: "userId", dataType: dataType.TEXT },
],
vectorizers: configure.vectorizer.text2VecOpenAI({
model: "text-embedding-3-small",
vectorizeCollectionName: false,
}),
generative: configure.generative.openAI({ model: "gpt-4o-mini" }),
})
}
}
Document Ingestion and Hybrid Search
// lib/weaviate/search.ts — insert, hybrid search, and generative
import { getWeaviateClient } from "./client"
import { COLLECTIONS } from "./schema"
import { chunkText } from "@/lib/ai/embeddings"
import { filters } from "weaviate-client"
export type ChunkObject = {
docId: string
chunkIndex: number
text: string
title: string
source: string
category?: string
userId?: string
}
/** Insert all chunks for a document */
export async function ingestDocument(doc: {
docId: string
title: string
content: string
source: string
category?: string
userId?: string
}): Promise<number> {
const client = await getWeaviateClient()
const chunks = chunkText(doc.content)
const objects: ChunkObject[] = chunks.map((text, i) => ({
docId: doc.docId,
chunkIndex: i,
text,
title: doc.title,
source: doc.source,
category: doc.category ?? "general",
userId: doc.userId ?? "public",
}))
const collection = client.collections.get(COLLECTIONS.CHUNKS)
const result = await collection.data.insertMany(objects)
if (result.hasErrors) {
console.error("[Weaviate ingest] errors:", result.errors)
throw new Error(`Ingestion failed for ${Object.keys(result.errors).length} chunks`)
}
return chunks.length
}
/** Hybrid search (BM25 + vector) with optional filters */
export async function hybridSearch(
query: string,
options: {
topK?: number
alpha?: number // 0 = BM25 only, 1 = vector only, 0.75 = recommended
category?: string
userId?: string
minScore?: number
} = {},
): Promise<Array<{ docId: string; title: string; text: string; source: string; score: number }>> {
const { topK = 8, alpha = 0.75, category, userId, minScore = 0.3 } = options
const client = await getWeaviateClient()
const collection = client.collections.get(COLLECTIONS.CHUNKS)
// Build filter chain
let filter = undefined
if (category && userId) {
filter = filters.and(
collection.filter.byProperty("category").equal(category),
filters.or(
collection.filter.byProperty("userId").equal(userId),
collection.filter.byProperty("userId").equal("public"),
),
)
} else if (category) {
filter = collection.filter.byProperty("category").equal(category)
} else if (userId) {
filter = filters.or(
collection.filter.byProperty("userId").equal(userId),
collection.filter.byProperty("userId").equal("public"),
)
}
const result = await collection.query.hybrid(query, {
alpha,
limit: topK,
returnMetadata: ["score"],
...(filter ? { filters: filter } : {}),
})
// De-duplicate by docId
const seen = new Set<string>()
return result.objects
.filter((obj) => {
const score = obj.metadata?.score ?? 0
if (score < minScore) return false
if (seen.has(obj.properties.docId as string)) return false
seen.add(obj.properties.docId as string)
return true
})
.map((obj) => ({
docId: obj.properties.docId as string,
title: obj.properties.title as string,
text: obj.properties.text as string,
source: obj.properties.source as string,
score: obj.metadata?.score ?? 0,
}))
}
/** Generative search — answer a question from document context */
export async function generateAnswer(query: string, userId?: string): Promise<{ answer: string; sources: string[] }> {
const client = await getWeaviateClient()
const collection = client.collections.get(COLLECTIONS.CHUNKS)
const filter = userId
? filters.or(
collection.filter.byProperty("userId").equal(userId),
collection.filter.byProperty("userId").equal("public"),
)
: undefined
const result = await collection.generate.nearText(
[query],
{
singlePrompt: `Using only the following context, answer this question: "${query}"\n\nContext: {text}\n\nAnswer concisely.`,
},
{
limit: 4,
returnMetadata: ["score"],
...(filter ? { filters: filter } : {}),
},
)
const answers = result.objects.map((o) => o.generated ?? "").filter(Boolean)
const sources = [...new Set(result.objects.map((o) => o.properties.source as string))]
return {
answer: answers[0] ?? "I could not find relevant information to answer this question.",
sources,
}
}
/** Delete all chunks for a document */
export async function deleteDocument(docId: string): Promise<void> {
const client = await getWeaviateClient()
const collection = client.collections.get(COLLECTIONS.CHUNKS)
await collection.data.deleteMany(collection.filter.byProperty("docId").equal(docId))
}
For the Pinecone alternative when a fully managed, serverless vector database with no infrastructure to operate, sub-millisecond query latency at billion-vector scale, and a simpler API without managing schemas is preferred — Pinecone is zero-ops while Weaviate gives more control over the data model, schema, and can be self-hosted for data residency requirements, see the Pinecone guide. For the Chroma alternative when a minimal, embedded vector store for local development, Jupyter notebooks, or small Python/JavaScript applications without a separate server process is needed — Chroma is the default for quick experiments while Weaviate handles production multi-tenant deployments with complex filter queries and generative search, see the Chroma guide. The Claude Skills 360 bundle includes Weaviate skill sets covering hybrid search, schema management, and generative search. Start with the free tier to try open-source vector search generation.