Index documents from Jira, Confluence, or local files into a local vector database and search them. All data stays on your machine.
Key features:
- Jira & Confluence (Server/Data Center and Cloud). Jira ticket = document, Confluence page = document
- Local files (.pdf, .pptx, .docx, etc.) via Unstructured
- No data sent to third parties (except when used as MCP with a remote AI agent)
- Hybrid search: vector search + BM25 keyword search, merged by Reciprocal Rank Fusion
- Incremental updates: no need to rebuild the full index each time
- Filter results by metafields (space, project, date, etc.)
- Ability to extend: add more data sources, search engines, embeddings, etc.
Technologies: SQLite (sqlite-vec and BM25), ChromaDB, sentence-transformers, Unstructured, LangChain
More context: Medium article
Contacts:
- Like it? Please star the repo
- Found a bug? Open an issue
- Want to contribute? - feel free to do it via fork and sending a pull request
- Want to chat? LinkedIn
- Check UPDATES.md for major updates.
- Some minor updates can be not added the file, so they can be found only in git history.
flowchart TD
A[1. Create collection] -->|loads & indexes documents| B["Collection stored in ./data/collections/${name}"]
B --> C{What next?}
C --> D[2. Update collection]
C --> E[3. Search via CLI]
C --> F[4. Search via MCP]
D -->|indexes only new/changed docs| B
E -->|search| B
F -->|search| B
- Create a collection — load and index documents from Jira, Confluence, or local files. Stored in
./data/collections/{name} - Update — re-index only new or changed documents (much faster than full creation)
- Search — find documents by text query via CLI
- MCP — expose search as a tool for AI agents
graph TD
A["./data/collections/${name}/"] --> B["documents/"]
A --> C["indexes/"]
A --> D["manifest.json"]
B --- B1["Loaded and converted documents"]
C --- C1["Vector and keyword index files"]
D --- D1["Collection metadata: name, last update time, reader config, index list"]
See ./main/core/documents_collection_creator.py for creation/update details and ./main/core/documents_collection_searcher.py for search details.
When you create a collection, you can specify a list of indexers like: --indexers "indexer_SqlLiteVector__embeddings_sentence-transformers_slash_all-MiniLM-L6-v2", "indexer_SqlLiteBM25" (it's also the default value). The indexers define what vector/keyword databases and embedding models are used. Database and embedding model are separated by __. For example:
indexer_SqlLiteVector__embeddings_sentence-transformers_slash_all-MiniLM-L6-v2means that SqlLite withsqlite-vecextension is used as vector database andsentence-transformers/all-MiniLM-L6-v2is used as the embedding model. You can use any embedding model from next list, you only need to add prefixembeddings_and replace slash symbols with_slash_. For example, if you want to use it with BAAI/bge-m3 embedder model, indexer name should be:indexer_SqlLiteVector__embeddings_BAAI_slash_bge-m3;indexer_SqlLiteBM25means that SqlLite BM25 is used as search engine.indexer_ChromaDb__embeddings_sentence-transformers_slash_all-MiniLM-L6-v2means thatChromaDbis used as vector database andsentence-transformers/all-MiniLM-L6-v2is used as the embedding model. Embedding model names follow the same rules as forindexer_SqlLiteVector.
You can define as many indexers as you want, their search results will be combined by Reciprocal Rank Fusion.
Either uv or docker can be used to run the project.
- Clone the repository
- Install uv
- Run
uv syncin the project root
A Dockerfile is included for running the tool without installing Python or uv locally.
Build the image:
docker build -t documents-vector-search .Run any command by passing it as arguments and mounting a local data/ folder:
docker run --rm \
-v $(pwd)/data:/app/data \
-e CONF_TOKEN="${yourToken}" \
documents-vector-search \
uv run dvs.py create-confluence \
--collection "confluence" \
--url "${baseConfluenceUrl}" \
--cql "${confluenceQuery}"- Mount
-v $(pwd)/data:/app/dataso collections and caches are persisted on your host machine - Pass credentials as
-e ENV_VAR=value(see Authentication) - The
data/folder structure (collections/,caches/,local_file_input/) is created automatically inside the container
Run the unified MCP HTTP server with Docker:
docker run --rm \
-p 8000:8000 \
-v $(pwd)/data:/app/data \
documents-vector-search \
uv run dvs.py mcp --http --httpPort 8000Then configure your MCP client to connect to http://localhost:8000/mcp.
All functionality is available through a single script:
uv run dvs.py <operation> [arguments]| Operation | Description |
|---|---|
create-confluence |
Create a collection from Confluence pages |
create-jira |
Create a collection from Jira tickets |
create-files |
Create a collection from local files |
update |
Re-index new and changed documents of an existing collection |
search |
Search in a collection from the command line |
fetch |
Fetch a document content from a collection by its id |
mcp |
Run unified MCP server (all collections, stdio or HTTP) |
mcp-single |
Run MCP stdio server for a single collection |
Every operation has its own arguments, each with a short and a long form (e.g. -c / --collection). Run uv run dvs.py <operation> --help to see all of them.
Set environment variables before creating or updating Jira/Confluence collections (not needed for local files):
| Platform | Type | Environment Variables |
|---|---|---|
| Confluence Server/DC | Bearer token (recommended) | CONF_TOKEN |
| Confluence Server/DC | Login/Password | CONF_LOGIN, CONF_PASSWORD |
| Confluence Cloud | Email/API token | ATLASSIAN_EMAIL, ATLASSIAN_TOKEN (get token) |
| Jira Server/DC | Bearer token (recommended) | JIRA_TOKEN |
| Jira Server/DC | Login/Password | JIRA_LOGIN, JIRA_PASSWORD |
| Jira Cloud | Email/API token | ATLASSIAN_EMAIL, ATLASSIAN_TOKEN (get token) |
Cloud vs Server is auto-detected: URLs ending with .atlassian.net are treated as Cloud.
uv run dvs.py create-confluence \
--collection "confluence" \
--url "${baseConfluenceUrl}" \
--cql "${confluenceQuery}"--collection— name of the collection (used later for update/search). Data stored in./data/collections/{name}--url— Confluence base URL (e.g.,https://confluence.example.comorhttps://your-domain.atlassian.net)--cql— Confluence query, e.g.,"(space = 'MySpace') AND (lastModified >= '2025-01-01')"
uv run dvs.py create-jira \
--collection "jira" \
--url "${baseJiraUrl}" \
--jql "${jiraQuery}"--url— Jira base URL (e.g.,https://jira.example.comorhttps://your-domain.atlassian.net)--jql— Jira query, e.g.,"project = MyProject AND created >= -183d"
uv run dvs.py create-files --basePath "${pathToFolder}"- Collection name defaults to the last folder name. Override with
--collection {name} - Unreadable files are skipped by default. Use
--failFastto stop on first error - Filter files with
--includePatterns "regex1" "regex2"and--excludePatterns "regex1" "regex2" - Uses Unstructured for parsing. Some formats may need extra software
uv run dvs.py update --collection "${collectionName}"uv run dvs.py search \
--collection "${collectionName}" \
--query "How to set up react project locally"--includeMatchedChunksText— include matched text chunks in results--filter— filter by metafields (see below)--rrfK— RRF constant for multi-index fusion (default:60)
Works with ChromaDB, SQLite BM25 and SQLite Vector indexes.
Syntax:
field operator "value" and/or field operator "value"
Operators: =, !=, >, >=, <, <=. Use and / or to join conditions (mixing both is not supported).
Confluence metafields:
| Field | Description |
|---|---|
space |
Space key |
createdAt |
Page creation date |
createdBy |
Creator email (lowercase) |
lastModifiedAt |
Last update date |
Examples:
--filter 'space = "SPACE_KEY"'
--filter 'space = "SPACE_KEY" and lastModifiedAt > "2026-01-01"'
--filter '(space = "SPACE_KEY1" or space = "SPACE_KEY2") and lastModifiedAt > "2026-01-01"'Jira metafields:
| Field | Description |
|---|---|
project |
Project key |
type |
Issue type (Bug, Task, Story, ...) |
status |
Status (Open, In Progress, Done, ...) |
priority |
Priority (High, Medium, Low, ...) |
epic |
Epic or parent issue key |
assignee |
Assignee email (lowercase) |
createdAt |
Issue creation date |
createdBy |
Creator email (lowercase) |
lastModifiedAt |
Last update date |
Examples:
--filter 'project = "PROJ"'
--filter 'project = "PROJ" and lastModifiedAt > "2026-01-01"'
--filter '(project = "PROJ1" or project = "PROJ2") and lastModifiedAt > "2026-01-01"'Files metafields:
| Field | Description |
|---|---|
createdAt |
File creation date |
lastModifiedAt |
Last modified date |
folder1 |
First subfolder of the file path |
folder2 |
Second subfolder of the file path |
folderN |
Nth subfolder of the file path (only present if path has N or more subfolders) |
Examples:
--filter 'folder1 = "docs"'
--filter 'folder1 = "docs" and folder2 = "api"'
--filter '(folder2 = "api" or folder2 = "presentations") and lastModifiedAt > "2026-01-01"'uv run dvs.py fetch \
--collection "${collectionName}" \
--id "${documentId}"--id— document ID to fetch (required)--startLine/--endLine— line range to return (default: 1–200)--format— output format:json,json_with_indent(default), ortoon
There are two MCP server operations:
| Operation | Best for | Key differences |
|---|---|---|
dvs.py mcp |
Modern AI models | All collections in one server. AI model chooses collection, filter and number of chunks. Supports stdio and HTTP transport. |
dvs.py mcp-single |
Simpler AI models or restricted setups | One collection per server. Collection, filter and other settings are hardcoded via CLI args. Stdio only. |
Add to your MCP config (e.g., .vscode/mcp.json for VS Code + GitHub Copilot):
{
"servers": {
"dvs": {
"type": "stdio",
"command": "uv",
"args": [
"--directory", "${fullPathToRootProjectFolder}",
"run", "dvs.py", "mcp"
]
}
}
}- All collections from
./data/collections/are available automatically - Use
--collections "name1" "name2"to limit which collections are exposed - Use
--rrfK {number}to tune Reciprocal Rank Fusion behavior for multi-index search - Use
--defaultNumberOfChunks {number}and--maxNumberOfChunks {number}to tune the number of text chunks returned by a single search
Or start as http server:
uv run dvs.py mcp --http --httpPort 8000And setup mcp like:
{
"servers": {
"dvs": {
"type": "http",
"url": "http://localhost:8000/mcp",
}
}
}{
"servers": {
"search_${collectionName}": {
"type": "stdio",
"command": "uv",
"args": [
"--directory", "${fullPathToRootProjectFolder}",
"run", "dvs.py", "mcp-single",
"--collection", "${collectionName}"
]
}
}
}- Replace
${collectionName}and${fullPathToRootProjectFolder}with real values - Use
--maxNumberOfChunks {number}to control how many text chunks are returned (more = better search, but may exceed model context window) - Use
--rrfK {number}to tune Reciprocal Rank Fusion behavior for multi-index search - Use
--filterfor metafield filtering (details)
Prompt examples:
- "Find info about AI use cases, search on Confluence, include all used links"
- "Find info about PDP carousel, search on Jira, include all used links"
If you develop the tool, you can run unit tests:
uv run pytest
- Incremental updates — only new/changed documents are re-indexed. Uses
lastModifiedDocumentTimefrommanifest.json(5 mins for Jira and Confluence buffer to avoid missing concurrent updates); - Caching — Jira/Confluence collection creation caches downloaded documents in
./data/caches/{hash}. Same parameters = same cache. If you need fresh data, either run an update after creation, or delete the cache folder manually; - there are more parameters in each operation, use
uv run dvs.py <operation> --helpto get more.