Skip to content

Commit 68558b1

Browse files
authored
docs(target): add document for creating new target (#886)
* docs(target): add document for creating new target * docs: fix broken links and add links to the trait.
1 parent 9db2a9c commit 68558b1

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: New Built-in Target
3+
description: Learn how to create a new built-in target in CocoIndex.
4+
toc_max_heading_level: 4
5+
---
6+
7+
import Tabs from '@theme/Tabs';
8+
import TabItem from '@theme/TabItem';
9+
10+
A target connects CocoIndex flow and external systems, and needs to synchronize changes of data from the CocoIndex flow to outside.
11+
12+
## Built-in Targets and Custom Targets
13+
14+
CocoIndex allows you to create [custom targets](/docs/custom_ops/custom_targets) by Python SDK.
15+
Custom targets share the gist as built-in targets, but the interface is simplified, so it only supports a subset of capabilities.
16+
The most notable difference is that custom target API exposed by Python SDK doesn't provide schema of the data to be exported, so it only fits most of the use cases where the target schema is fixed.
17+
But the built-in targets get the schema, so it can be used to define general-purpose targets.
18+
19+
You're recommended to read the [custom target documentation](/docs/custom_ops/custom_targets) first as a starting point.
20+
21+
## Life of a Built-in Target
22+
23+
The core logic of a built-in target is the *Target Connector*, which is an implementation of the trait [`TargetFactoryBase`](https://github.com/search?q=repo%3Acocoindex-io%2Fcocoindex%20%22trait%20TargetFactoryBase%22&type=code).
24+
25+
*Users* provide a *Target Spec* when they export specific data (in the form of [Data Collector](/docs/core/flow_def#data-collector)) to the target.
26+
27+
```mermaid
28+
flowchart TD
29+
FlowAnalyzer[CocoIndex Flow Analyzer] --> DataSchema[Data Schema]
30+
ExecutionEngine[CocoIndex Execution Engine] --> Mutations[mutations]
31+
32+
TargetSpec[Target Spec] --> BuildMethod(build#40;#41;)
33+
DataSchema --> BuildMethod
34+
35+
BuildMethod --> SetupKeyState[Setup Key & State]
36+
BuildMethod --> ExportContext[Export Context]
37+
38+
SetupKeyState --> InternalStorage[(Internal Storage)]
39+
SetupKeyState --> DiffMethod(diff_setup_states#40;#41;)
40+
InternalStorage --> |previous versions| DiffMethod
41+
42+
DiffMethod --> SetupChange[Setup Change]
43+
44+
SetupChange --> ApplySetupMethod(apply_setup_changes#40;#41;)
45+
ApplySetupMethod --> |setup changes #40;DDL#41;| TargetInfra[Target Infrastructure]
46+
47+
ExportContext --> ApplyMutationMethod(apply_mutation#40;#41;)
48+
Mutations --> ApplyMutationMethod
49+
ApplyMutationMethod --> |data changes #40;DML#41;| TargetInfra
50+
51+
%% Styling
52+
classDef cocoClass fill:#aaa,color:black
53+
classDef storageClass fill:#aaa,color:black
54+
classDef methodClass fill:#ffc,color:black,font-weight:bold
55+
56+
class BuildMethod,DiffMethod,ApplySetupMethod,ApplyMutationMethod methodClass
57+
class InternalStorage,TargetInfra storageClass
58+
class TargetSpec,SetupKeyState,ExportContext,SetupChange,DataSchema,Mutations dataClass
59+
class FlowAnalyzer,ExecutionEngine cocoClass
60+
```
61+
62+
63+
### States / Contexts Building
64+
65+
*CocoIndex* analyzes the flow before execution. For each target, it will have the information of *Data Schema* exported to the target, and the *Target Spec*.
66+
The *Target Connector* (an implementation of `TargetFactoryBase`) takes them by the `build()` method, to digest this information and return necessary information for setup changes and data changes, including a *Setup Key*, a *Setup State* and a *Export Context* for each target.
67+
68+
*Setup Key* and *Setup State* provides information to decide how to setup the target.
69+
Both *Setup Key* and *Setup State* are persisted in CocoIndex's [internal storage](/docs/core/basics#internal-storage), to keep track of targets' states.
70+
71+
- *Setup Key* is a key used to uniquely identify the target. It should remain stable across different calls.
72+
If it changes, CocoIndex considers it as a different target.
73+
For example, for Postgres, we use the table name as the setup key.
74+
75+
- *Setup State* is a state of the target.
76+
*Setup Key* and *Setup State* should contain sufficient information to decide how to setup the target.
77+
For example, for Postgres, we use the table name and its schema as the setup state.
78+
79+
:::note
80+
81+
Note that different CocoIndex data types may be projected to the same column type in Postgres, e.g. all _Struct_ and _Table_ types are projected to `jsonb`.
82+
It's sufficient to keep `jsonb` instead of the specific CocoIndex type in *Setup State*, so the *Setup State* doesn't need to be changed too often.
83+
84+
For historical reasons, we already put the specific CocoIndex type in *Setup State* for Postgres targets, and it's not easy to update it with a smooth migration, so we keep it for now.
85+
But for new targets, it's recommended to keep just sufficient information to define the target.
86+
87+
:::
88+
89+
*Export Context* is an in-memory object that contains necessary information and objects to apply data changes.
90+
For example, it may hold a database connection pool, or a prepared statement for upserting/deleting data, etc.
91+
92+
### Apply Setup Changes
93+
94+
*CocoIndex* calls the *Target Connector* (`diff_setup_states()`) to compare the current *Setup State* with previous ones, which returns *Setup Change* object to describe the change (or no change).
95+
For example, for Postgres, the *Setup Change* object contains information such as which table needs to be created/dropped, which column needs to be added/dropped, etc.
96+
97+
*CocoIndex* further passes the *Setup Change* to the *Target Connector* (`apply_setup_changes()`), which is responsible for applying the *Setup Change* to the target infrastructure.
98+
Here's the place for the target connector to issue the actual commands to setup, drop or update the target infrastructure.
99+
100+
:::note
101+
102+
Here CocoIndex may pass multiple versions of possible existing states to the target connector via `diff_setup_states()`. This is because there may be failures or interrupts in the previous setup attempts, so CocoIndex doesn't always clearly know the exact state of the target. CocoIndex needs to keep multiple versions of states on track (the existing one and the pending ones) until the next `apply_setup_changes()` is successful.
103+
104+
For example, consider the following scenarios:
105+
106+
- A Postgres target's initial state describes it has a column `col_1`.
107+
- Later, after the flow changes, a new column `col_2` is collected, so a new version of target state has columns `col_1` and `col_2`. But the attempt to apply this setup change doesn't go through (e.g. connection to the database is broken, in general CocoIndex doesn't know the exact state the backend ends up with).
108+
- Now the user updates the column name from `col_2` to `col_3` in their flow. In the next attempt to apply the setup change, we get a setup state with `col_1` and `col_3`.
109+
110+
- To apply setup changes for the latest, CocoIndex will pass two versions existing setup states to the target connector via `diff_setup_states()`, one with `col_1`, the other with `col_1` and `col_2`. The new setup state has `col_1` and `col_3`. And the target connector needs to create a setup change that take both of the possible existing states into account. The change should be "drop `col_2` and add `col_3`".
111+
- When applying the setup change in `apply_setup_changes()`, the target connector should do this idempotently, i.e. it should be a no-op if the column `col_2` already doesn't exist (by `DROP IF EXISTS`), so it works well on either possible existing state.
112+
- After this setup change is applied successfully, CocoIndex clearly knows the target's current state (i.e. with columns `col_1` and `col_3`), and safely forgets all previous states.
113+
114+
:::
115+
116+
### Apply Data Changes
117+
118+
*CocoIndex* calls the *Target Connector* (`apply_mutation()`) to apply data changes, by list of mutations (upserts and deletes) together with their *Export Context*.
119+
The method should apply these mutations in an idempotent way, i.e. it should be a no-op if the mutation is already applied.
120+
For example, for Postgres, upserts are done by `INSERT ... ON CONFLICT ... DO UPDATE SET ...`, and deletes are done by `DELETE ... WHERE ...`.
121+
122+
### Notes on Multiple Targets
123+
124+
The interface of multiple methods in `TargetFactoryBase` (`build()`, `apply_setup_changes()`, `apply_mutation()`) takes a batch of inputs coming from multiple targets of the same type. This gives a chance for the connector to handle dependencies between them (if any) and apply changes in the correct order.
125+
For example, to support property graph databases, relationships depend on nodes, so nodes need to be added before relationships, and on deletion it happens in reverse order.
126+
This is not a consideration for target types that different targets are independent of each other.
127+
128+
## Examples
129+
130+
The following target implementations provide good examples:
131+
132+
- [Postgres](/docs/ops/targets#postgres), see [related code](https://github.com/search?q=repo%3Acocoindex-io%2Fcocoindex+path%3A%2Ftarget%2F+Postgres&type=code).
133+
It provides a good example for targets with specific column types in the schema.
134+
135+
- [Qdrant](/docs/ops/targets#qdrant), see [related code](https://github.com/search?q=repo%3Acocoindex-io%2Fcocoindex+path%3A%2Ftarget%2F+Qdrant&type=code).
136+
It provides a good example for targets without specific column types in the schema, as Qdrant's payloads are JSON objects.

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const sidebars: SidebarsConfig = {
7373
items: [
7474
'contributing/guide',
7575
'contributing/setup_dev_environment',
76+
'contributing/new_built_in_target',
7677
],
7778
},
7879
{

0 commit comments

Comments
 (0)