AutomationFlowsAI & RAG › Build Hybrid RAG Search Over Pdfs with Qdrant and Ollama

Build Hybrid RAG Search Over Pdfs with Qdrant and Ollama

ByChristo @chrmirchev on n8n.io

This workflow ingests a local PDF into Qdrant with Ollama embeddings, then supports hybrid retrieval by querying Qdrant with both dense vectors and BM25 sparse vectors from an n8n chat trigger. Starts manually to read a PDF from disk and extract its text content. Checks whether…

Event trigger★★★★☆ complexityAI-powered23 nodesRead Write FileN8N Nodes QdrantOllama EmbeddingsQdrant Vector StoreDocument Default Data LoaderText Splitter Recursive Character Text SplitterChat TriggerHTTP Request
AI & RAG Trigger: Event Nodes: 23 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #16040 — we link there as the canonical source.

This workflow follows the Chat Trigger → Documentdefaultdataloader recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "7QGW6MJQ7rtu6yOx",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Hybrid RAG",
  "tags": [
    {
      "id": "i7ke2xhzqhiMPaqj",
      "name": "PRO",
      "createdAt": "2026-04-24T14:13:44.739Z",
      "updatedAt": "2026-04-24T14:13:44.739Z"
    }
  ],
  "nodes": [
    {
      "id": "f7aab1f6-679a-4c4f-948e-4a11082eddee",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "341b89c3-3f60-4106-97ba-a6f3bf3ace2a",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        416,
        0
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7f184fd6-abab-4829-9bf1-21d0b9ba17bb",
      "name": "Read/Write Files from Disk",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        208,
        0
      ],
      "parameters": {
        "options": {},
        "fileSelector": "/tmp/n8n_Self_Hosted_Enterprise_Terms_and_Conditions.pdf"
      },
      "typeVersion": 1.1
    },
    {
      "id": "0b1f384a-a1f1-4907-851c-0d7555624f74",
      "name": "Check If Collection Exists",
      "type": "n8n-nodes-qdrant.qdrant",
      "position": [
        624,
        -192
      ],
      "parameters": {
        "operation": "collectionExists",
        "collectionName": "testing",
        "requestOptions": {}
      },
      "credentials": {
        "qdrantRestApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "cffaa658-ffcd-4636-b5ca-dd9608edce02",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        784,
        -192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c00f0ddf-a88e-4bf0-a7eb-9e0873bf426e",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.result.exists }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "fa01d0a3-5e05-4160-b6e2-e3210cddd025",
      "name": "Create Collection",
      "type": "n8n-nodes-qdrant.qdrant",
      "position": [
        976,
        -208
      ],
      "parameters": {
        "vectors": "{\n      \"size\":768,\n      \"distance\":\"Cosine\"\n}",
        "operation": "createCollection",
        "shardNumber": 1,
        "onDiskPayload": true,
        "sparseVectors": "{\n    \"sparse-text\": \n    {\n      \"modifier\": \"idf\",\n      \"model\": \"qdrant/bm25\"\n    }\n}",
        "collectionName": "testing",
        "requestOptions": {},
        "replicationFactor": 1,
        "writeConsistencyFactor": 1
      },
      "credentials": {
        "qdrantRestApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "28627d18-0978-4be9-a1b4-de614eca2865",
      "name": "Embeddings Ollama",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOllama",
      "position": [
        1232,
        256
      ],
      "parameters": {
        "model": "nomic-embed-text:latest"
      },
      "credentials": {
        "ollamaApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c23f9282-1e7c-4f8d-b88e-9869fc52b392",
      "name": "Qdrant Vector Store",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "position": [
        1312,
        -16
      ],
      "parameters": {
        "mode": "insert",
        "options": {},
        "qdrantCollection": {
          "__rl": true,
          "mode": "id",
          "value": "testing"
        }
      },
      "credentials": {
        "qdrantApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "a917fa5e-f493-4e64-a52e-c8e591f8d987",
      "name": "Default Data Loader",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "position": [
        1360,
        256
      ],
      "parameters": {
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "title",
                "value": "={{ $json.info.Title }}"
              },
              {
                "name": "file_name",
                "value": "={{ $('Read/Write Files from Disk').item.json.fileName }}"
              }
            ]
          }
        },
        "textSplittingMode": "custom"
      },
      "typeVersion": 1.1
    },
    {
      "id": "90f777dc-cf30-4391-b349-f0e669181a6a",
      "name": "Recursive Character Text Splitter",
      "type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "position": [
        1424,
        432
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "53adf48b-dfd3-41d8-8c97-f9de991cb339",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1104,
        -16
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "ce27d2cb-c105-4194-b1e9-4df27ec7456d",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        -16,
        720
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.4
    },
    {
      "id": "edfb2256-fc5d-4f58-a3ea-926c7fe53742",
      "name": "Split Out",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        240,
        288
      ],
      "parameters": {
        "include": "=",
        "options": {},
        "fieldToSplitOut": "result.points"
      },
      "typeVersion": 1
    },
    {
      "id": "bbb209fb-07d7-4209-bfbf-cf5dfcff1868",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        672,
        288
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "82737c43-7a1b-49a6-baa4-766dfa35b7f0",
      "name": "Update Vectors",
      "type": "n8n-nodes-qdrant.qdrant",
      "position": [
        864,
        288
      ],
      "parameters": {
        "points": "={{ JSON.stringify($json.data) }}",
        "resource": "vector",
        "operation": "updateVectors",
        "collectionName": {
          "__rl": true,
          "mode": "list",
          "value": "testing",
          "cachedResultName": "testing"
        },
        "requestOptions": {}
      },
      "credentials": {
        "qdrantRestApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "98215fd4-8760-47d3-88c7-813fd449d1b6",
      "name": "Get All Points",
      "type": "n8n-nodes-qdrant.qdrant",
      "position": [
        0,
        288
      ],
      "parameters": {
        "resource": "point",
        "operation": "scrollPoints",
        "collectionName": {
          "__rl": true,
          "mode": "list",
          "value": "testing",
          "cachedResultName": "testing"
        },
        "requestOptions": {}
      },
      "credentials": {
        "qdrantRestApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1dbd1886-0e90-49a8-b63a-3f2f428ba2f2",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        464,
        288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "9caabe97-90d9-444e-beb9-8af981f95bbd",
              "name": "id",
              "type": "string",
              "value": "={{ $json[\"result.points\"].id }}"
            },
            {
              "id": "3fd1f0b3-4e5f-4324-890d-2892d8234e98",
              "name": "vector.sparse-text",
              "type": "object",
              "value": "={{ \n  {\n    \"text\": $json[\"result.points\"]?.payload?.content,\n    \"model\": \"qdrant/bm25\"\n  } \n}}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "40957992-5984-4b11-b33f-e91e7e9741a9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 1696,
        "height": 1040,
        "content": "## Hybrid Search on the n8n T&C\n\nDeployed on a self-hosted n8n framework with Qdrant, Ollama.\n\n**How it works**\n\n*   **Data Preparation**: \n     - Uses Qdrant Vector Store node in order to utilize the Recursive Text Splitter \nsubnode and generate embeddings of the sample legal document (n8n T&C). \n     - Make use of Qdrant to generate and add sparse vectors (qdrant/bm25 model) \n*   **Query the text to retrieve Qdrant points using hybrid search**\n\n### Key Strengths of This Blueprint\n- ***No Overwrite Safety***: By leveraging *updateVectors* on the final node \ninstead of a standard upsert, it successfully evades Qdrant's default behavior \nof erasing existing fields during data updates.\n- ***Hybrid Search Foundation***: Preparing the database schema using explicit\n dense settings paired side-by-side with an idf modified BM25 sparseVectors \nstructure unlocks both semantic meaning and exact keyword indexing capability."
      },
      "typeVersion": 1
    },
    {
      "id": "0398cd34-c985-47ad-85d2-b5178a7c6894",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        608
      ],
      "parameters": {
        "color": 2,
        "width": 784,
        "height": 336,
        "content": "## Test hybrid search by querying the the text"
      },
      "typeVersion": 1
    },
    {
      "id": "a3b6f454-bca8-467e-bdaf-9d3d654285c7",
      "name": "Generate the embeddings of the query",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        192,
        720
      ],
      "parameters": {
        "url": "http://host.docker.internal:11434/api/embeddings",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"nomic-embed-text:latest\",\n  \"prompt\": \"{{ $json.chatInput }}\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.4
    },
    {
      "id": "ba5aa1df-bb37-4182-836f-dd93db710833",
      "name": "Query Points (using the embeddings)",
      "type": "n8n-nodes-qdrant.qdrant",
      "position": [
        416,
        720
      ],
      "parameters": {
        "query": "={\n  \"fusion\": \"rrf\"\n}",
        "prefetch": "=[\n  {\n    \"query\": [{{ $json.embedding }}],\n    \"using\": \"\",\n    \"limit\": 25\n  },\n  {\n    \"query\": {\n      \"text\": \"{{ $('When chat message received').item.json.chatInput }}\",\n      \"model\": \"qdrant/bm25\"\n    },\n    \"using\": \"sparse-text\",\n    \"limit\": 25\n  }\n]",
        "resource": "search",
        "operation": "queryPoints",
        "collectionName": {
          "__rl": true,
          "mode": "list",
          "value": "testing",
          "cachedResultName": "testing"
        },
        "requestOptions": {}
      },
      "credentials": {
        "qdrantRestApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "eadb5b7f-1043-4e20-87ba-3aef3dab6470",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        768,
        608
      ],
      "parameters": {
        "color": 5,
        "width": 864,
        "height": 624,
        "content": "## Core Workflow Breakdown\n**1. Document Extraction & Guardrails (Top Left)**\n- *Manual Trigger*: Starts the ingestion process.\n- *Read/Write Files from Disk*: Pulls an enterprise terms-and-conditions PDF from a designated directory (/tmp/).\n- *Extract from File*: Parses the raw text content out of the PDF file. \n- *Check If Collection Exists & If Nodes*: Queries the Qdrant instance for a collection named \"testing\". If it doesn't exist, the workflow diverts to the Create Collection node.\n- *Create Collection*: Programmatically builds the database infrastructure configured for true hybrid search:\n    - Dense Vectors: 768 dimensions using Cosine distance (perfect for nomic-embed-text).\n    - Sparse Vectors: A named sparse index (\"sparse-text\") using Qdrant\u2019s native BM25 model for precise keyword matching.\n\n**2. Advanced AI Vector Ingestion (Top Right)**\n- *Merge*: Dynamically reconnects the infrastructure check branch back to the parsed document stream.\n- *Qdrant Vector Store (Advanced AI Branch)*: Acts as the data orchestrator feeding the underlying sub-nodes.\n- *Default Data Loader*: Takes the parsed text and injects metadata (Document Title and File Name).\n- *Recursive Character Text Splitter*: Intelligently chunks the document text to avoid model token limits.\n- *Embeddings Ollama*: Runs the local *nomic-embed-text* model to convert the text chunks into dense 768-dimension vectors and pushes them straight into Qdrant.\n\n**3. Sparse Vector Engine & Chat Sync (Bottom Loop)**\n- *When Chat Message Received*: An active webhook webhook node waiting for user queries to fire downstream processes.\n- *Code Node (\"Extract sparse from Qdrant\")*: Uses a custom internal HTTP helper to perform a native REST API POST payload call to /points/scroll. It specifically fetches the points while extracting only the \"sparse-text\" array.\n- *Split Out*: Takes the raw array of points returned from Qdrant and splits them into distinct n8n payload items.\n- *Aggregate*: Re-combines processed elements into a single uniform structural dataset.\n- *Qdrant Update Node*: Executes an updateVectors operation using JSON.stringify($json.data). This ensures that any calculated vector or payload additions map precisely onto existing point IDs without completely overwriting or corrupting the pre-existing dense vector spaces."
      },
      "typeVersion": 1
    },
    {
      "id": "42b33a70-b960-41ba-8945-de401640cfba",
      "name": "Extract sparse from Qdrant",
      "type": "n8n-nodes-base.code",
      "position": [
        -32,
        1008
      ],
      "parameters": {
        "jsCode": "const qdrantUrl = 'http://host.docker.internal:6333'; // Update this to your instance endpoint\nconst collectionName = 'testing'; // Put your actual collection name here\n\n// Using n8n's native context helper for direct REST interaction\nconst response = await this.helpers.httpRequest({\n  method: 'POST',\n  url: `${qdrantUrl}/collections/${collectionName}/points/scroll`,\n  headers: {\n    'Content-Type': 'application/json',\n    // 'api-key': 'YOUR_KEY' // Uncomment this specific line if you are using Qdrant Cloud\n  },\n  body: JSON.stringify({\n    limit: 10,\n    with_payload: false,\n    with_vector: [\"sparse-text\"]\n  }),\n  json: true // Forces the helper to automatically parse the return payload as JSON\n});\n\n// Extract points and format them safely into an n8n-compliant iterable array\nconst points = response.result?.points || [];\n\nreturn points.map(point => ({ json: point }));\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "1fe1cf1f-6018-445a-952f-54bd8a76f0e3",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Create Collection",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Update Vectors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Points": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Collection": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings Ollama": {
      "ai_embedding": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Check If Collection Exists",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Default Data Loader": {
      "ai_document": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "ai_document",
            "index": 0
          }
        ]
      ]
    },
    "Qdrant Vector Store": {
      "main": [
        [
          {
            "node": "Get All Points",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Collection Exists": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read/Write Files from Disk": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Generate the embeddings of the query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recursive Character Text Splitter": {
      "ai_textSplitter": [
        [
          {
            "node": "Default Data Loader",
            "type": "ai_textSplitter",
            "index": 0
          }
        ]
      ]
    },
    "Query Points (using the embeddings)": {
      "main": [
        []
      ]
    },
    "Generate the embeddings of the query": {
      "main": [
        [
          {
            "node": "Query Points (using the embeddings)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Read/Write Files from Disk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow ingests a local PDF into Qdrant with Ollama embeddings, then supports hybrid retrieval by querying Qdrant with both dense vectors and BM25 sparse vectors from an n8n chat trigger. Starts manually to read a PDF from disk and extract its text content. Checks whether…

Source: https://n8n.io/workflows/16040/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Build A Financial Documents Assistant Using Qdrant And Mistral.Ai. Uses localFileTrigger, manualTrigger, stickyNote, readWriteFile. Event-driven trigger; 29 nodes.

Local File Trigger, Read Write File, Embeddings Mistral Cloud +8
AI & RAG

Localfile. Uses localFileTrigger, manualTrigger, stickyNote, readWriteFile. Event-driven trigger; 29 nodes.

Local File Trigger, Read Write File, Embeddings Mistral Cloud +8
AI & RAG

This n8n workflow demonstrates how to manage your Qdrant vector store when there is a need to keep it in sync with local files. It covers creating, updating and deleting vector store records ensuring

Local File Trigger, Read Write File, Embeddings Mistral Cloud +8
AI & RAG

An on-premises, domain-specific AI assistant for Kaggle (tested on binary disaster-tweet classification), combining LLM, an n8n workflow engine, and Qdrant-backed Retrieval-Augmented Generation (RAG).

Local File Trigger, Document Default Data Loader, Text Splitter Recursive Character Text Splitter +10
AI & RAG

Code Extractfromfile. Uses manualTrigger, sort, httpRequest, compression. Event-driven trigger; 50 nodes.

HTTP Request, Compression, Edit Image +15