{
  "name": "AppFlowy Direct Query Tool",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "appflowy-query-tool",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "appflowy-query-webhook",
      "name": "AppFlowy Query Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "query-text",
              "name": "queryText",
              "value": "={{ $json.body.query || $json.body.searchTerm || '' }}",
              "type": "string"
            },
            {
              "id": "query-type",
              "name": "queryType",
              "value": "={{ $json.body.queryType || 'semantic_search' }}",
              "type": "string"
            },
            {
              "id": "database-filter",
              "name": "databaseFilter",
              "value": "={{ $json.body.databaseFilter || [] }}",
              "type": "array"
            },
            {
              "id": "workspace-filter",
              "name": "workspaceFilter",
              "value": "={{ $json.body.workspaceFilter || $vars.appflowy_default_workspace }}",
              "type": "string"
            },
            {
              "id": "result-limit",
              "name": "resultLimit",
              "value": "={{ $json.body.limit || 10 }}",
              "type": "number"
            },
            {
              "id": "include-metadata",
              "name": "includeMetadata",
              "value": "={{ $json.body.includeMetadata || true }}",
              "type": "boolean"
            }
          ]
        },
        "options": {}
      },
      "id": "process-appflowy-query",
      "name": "Process AppFlowy Query",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        450,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "semantic-search-condition",
              "leftValue": "={{ $json.queryType }}",
              "rightValue": "semantic_search",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-query-type",
      "name": "Check Query Type",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        700,
        400
      ]
    },
    {
      "parameters": {
        "model": "nomic-embed-text:latest"
      },
      "id": "generate-query-embedding",
      "name": "Generate Query Embedding",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOllama",
      "typeVersion": 1,
      "position": [
        950,
        300
      ],
      "credentials": {
        "ollamaApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "code": {
          "execute": {
            "code": "const { QdrantVectorStore } = require(\"@langchain/qdrant\");\nconst { OllamaEmbeddings } = require(\"@langchain/community/embeddings/ollama\");\n\nconst embeddings = new OllamaEmbeddings({\n  model: \"nomic-embed-text\",\n  baseUrl: \"http://ollama:11434\"\n});\n\nconst vectorStore = await QdrantVectorStore.fromExistingCollection(\n  embeddings,\n  {\n    url: \"http://qdrant:6333\",\n    collectionName: \"knowledge_base\",\n  }\n);\n\nconst queryText = this.getInputData()[0].json.queryText;\nconst resultLimit = this.getInputData()[0].json.resultLimit || 10;\n\n// Create filter for AppFlowy content only\nconst filter = {\n  must: [\n    {\n      key: \"metadata.source\",\n      match: {\n        value: \"appflowy\",\n      },\n    },\n  ],\n};\n\n// Perform semantic search\nconst results = await vectorStore.similaritySearch(queryText, resultLimit, filter);\n\n// Format results\nconst formattedResults = results.map((doc, index) => ({\n  id: index,\n  content: doc.pageContent,\n  metadata: doc.metadata,\n  source: 'vector_search',\n  relevanceScore: doc.score || 0\n}));\n\nreturn [{ json: { results: formattedResults, queryType: 'semantic_search', totalResults: formattedResults.length } }];"
          }
        },
        "inputs": {
          "input": [
            {
              "type": "main",
              "required": true
            }
          ]
        },
        "outputs": {
          "output": [
            {
              "type": "main"
            }
          ]
        }
      },
      "id": "perform-semantic-search",
      "name": "Perform Semantic Search",
      "type": "@n8n/n8n-nodes-langchain.code",
      "typeVersion": 1,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "operation": "listDatabases",
        "workspaceId": "={{ $('Process AppFlowy Query').item.json.workspaceFilter }}",
        "options": {}
      },
      "id": "get-appflowy-databases",
      "name": "Get AppFlowy Databases",
      "type": "n8n-nodes-appflowy.database",
      "typeVersion": 1,
      "position": [
        950,
        500
      ],
      "credentials": {
        "appFlowyApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fieldsToSplitOut": "databases",
        "options": {}
      },
      "id": "split-databases",
      "name": "Split Databases",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1200,
        500
      ]
    },
    {
      "parameters": {
        "operation": "listRows",
        "databaseId": "={{ $json.id }}",
        "options": {
          "limit": "={{ $('Process AppFlowy Query').item.json.resultLimit }}"
        }
      },
      "id": "get-database-rows",
      "name": "Get Database Rows",
      "type": "n8n-nodes-appflowy.database",
      "typeVersion": 1,
      "position": [
        1450,
        500
      ],
      "credentials": {
        "appFlowyApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "filter-rows",
              "name": "filteredRows",
              "value": "={{ $json.rows.filter(row => {\n  const queryText = $('Process AppFlowy Query').item.json.queryText.toLowerCase();\n  const searchableText = (row.title || '').toLowerCase() + ' ' + (row.content || '').toLowerCase() + ' ' + Object.values(row.fields || {}).join(' ').toLowerCase();\n  return searchableText.includes(queryText);\n}) }}",
              "type": "array"
            },
            {
              "id": "database-context",
              "name": "databaseContext",
              "value": "={{ { id: $json.id, name: $json.name, type: 'appflowy_database' } }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "filter-and-format-rows",
      "name": "Filter and Format Rows",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1700,
        500
      ]
    },
    {
      "parameters": {
        "fieldsToSplitOut": "filteredRows",
        "options": {}
      },
      "id": "split-filtered-rows",
      "name": "Split Filtered Rows",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1950,
        500
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "format-direct-result",
              "name": "formattedResult",
              "value": "={{ {\n  id: $json.id,\n  content: ($json.title || '') + '\\n\\n' + ($json.content || '') + '\\n\\nFields: ' + JSON.stringify($json.fields || {}),\n  metadata: {\n    source: 'appflowy',\n    type: 'database_row',\n    database_id: $('Filter and Format Rows').item.json.databaseContext.id,\n    database_name: $('Filter and Format Rows').item.json.databaseContext.name,\n    row_id: $json.id,\n    title: $json.title,\n    created_at: $json.created_at,\n    updated_at: $json.updated_at\n  },\n  source: 'direct_query',\n  relevanceScore: 1.0\n} }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "format-direct-query-result",
      "name": "Format Direct Query Result",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2200,
        500
      ]
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "formattedResult"
            }
          ]
        },
        "options": {}
      },
      "id": "aggregate-direct-results",
      "name": "Aggregate Direct Results",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        2450,
        500
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "combine-all-results",
              "name": "allResults",
              "value": "={{ [...($('Perform Semantic Search').all().flatMap(item => item.results || [])), ...($('Aggregate Direct Results').all().flatMap(item => item.formattedResult || []))] }}",
              "type": "array"
            },
            {
              "id": "query-summary",
              "name": "querySummary",
              "value": "={{ {\n  queryText: $('Process AppFlowy Query').item.json.queryText,\n  queryType: $('Process AppFlowy Query').item.json.queryType,\n  totalResults: $json.allResults.length,\n  semanticResults: ($('Perform Semantic Search').all().flatMap(item => item.results || [])).length,\n  directResults: ($('Aggregate Direct Results').all().flatMap(item => item.formattedResult || [])).length,\n  timestamp: $now\n} }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "combine-all-results",
      "name": "Combine All Results",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2700,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ranked-results",
              "name": "rankedResults",
              "value": "={{ $json.allResults.sort((a, b) => b.relevanceScore - a.relevanceScore).slice(0, $('Process AppFlowy Query').item.json.resultLimit) }}",
              "type": "array"
            },
            {
              "id": "response-metadata",
              "name": "responseMetadata",
              "value": "={{ {\n  queryProcessed: $json.querySummary.queryText,\n  resultsFound: $json.rankedResults.length,\n  queryType: $json.querySummary.queryType,\n  includeMetadata: $('Process AppFlowy Query').item.json.includeMetadata,\n  timestamp: $now\n} }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "id": "rank-and-limit-results",
      "name": "Rank and Limit Results",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2950,
        400
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "table": {
          "__rl": true,
          "value": "query_analytics",
          "mode": "list"
        },
        "data": {
          "insert": [
            {
              "column": "query_text",
              "value": "={{ $('Process AppFlowy Query').item.json.queryText }}"
            },
            {
              "column": "query_type",
              "value": "appflowy_direct"
            },
            {
              "column": "results_count",
              "value": "={{ $json.rankedResults.length }}"
            },
            {
              "column": "response_time_ms",
              "value": "={{ $now - $('Process AppFlowy Query').item.json.timestamp }}"
            },
            {
              "column": "query_metadata",
              "value": "={{ $json.responseMetadata }}"
            },
            {
              "column": "created_at",
              "value": "={{ $now }}"
            }
          ]
        },
        "options": {}
      },
      "id": "log-query-analytics",
      "name": "Log Query Analytics",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        3200,
        600
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "respond-to-appflowy-query",
      "name": "Respond to AppFlowy Query",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3200,
        400
      ]
    },
    {
      "parameters": {
        "content": "## AppFlowy Direct Query Tool\n\n**Purpose:** Direct querying interface for AppFlowy databases and content with both semantic and direct search capabilities.\n\n**Features:**\n- **Semantic Search**: Vector-based similarity search using embeddings\n- **Direct Query**: Text-based filtering across AppFlowy databases\n- **Result Ranking**: Relevance scoring and result prioritization\n- **Analytics**: Query performance tracking and usage analytics\n- **Flexible Filtering**: Database, workspace, and content type filters\n\n**Query Types:**\n- `semantic_search`: Vector similarity search (default)\n- `direct_query`: Text-based database search\n- `hybrid`: Combination of both approaches\n\n**API Parameters:**\n- `query`: Search text/question\n- `queryType`: Type of search to perform\n- `databaseFilter`: Specific databases to search\n- `workspaceFilter`: Target workspace ID\n- `limit`: Maximum results to return\n- `includeMetadata`: Include detailed metadata\n\n**Response Format:**\n```json\n{\n  \"rankedResults\": [...],\n  \"responseMetadata\": {...}\n}\n```",
        "height": 600,
        "width": 700,
        "color": 4
      },
      "id": "appflowy-query-documentation",
      "name": "AppFlowy Query Documentation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        100,
        100
      ]
    }
  ],
  "connections": {
    "AppFlowy Query Webhook": {
      "main": [
        [
          {
            "node": "Process AppFlowy Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process AppFlowy Query": {
      "main": [
        [
          {
            "node": "Check Query Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Query Type": {
      "main": [
        [
          {
            "node": "Perform Semantic Search",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get AppFlowy Databases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Perform Semantic Search": {
      "main": [
        [
          {
            "node": "Combine All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get AppFlowy Databases": {
      "main": [
        [
          {
            "node": "Split Databases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Databases": {
      "main": [
        [
          {
            "node": "Get Database Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Database Rows": {
      "main": [
        [
          {
            "node": "Filter and Format Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter and Format Rows": {
      "main": [
        [
          {
            "node": "Split Filtered Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Filtered Rows": {
      "main": [
        [
          {
            "node": "Format Direct Query Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Direct Query Result": {
      "main": [
        [
          {
            "node": "Aggregate Direct Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Direct Results": {
      "main": [
        [
          {
            "node": "Combine All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine All Results": {
      "main": [
        [
          {
            "node": "Rank and Limit Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank and Limit Results": {
      "main": [
        [
          {
            "node": "Respond to AppFlowy Query",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Query Analytics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "appflowy-query-v1",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "AppFlowyQueryTool",
  "tags": [
    "appflowy",
    "query",
    "search",
    "tool",
    "semantic"
  ]
}