{
  "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
          }
        ]
      ]
    }
  }
}