mirror of
https://github.com/karust/openserp.git
synced 2026-08-05 16:53:54 +08:00
Add OpenSERP SDK usage examples
This commit is contained in:
@@ -69,9 +69,9 @@ Once the server is running, the interactive docs are available locally:
|
||||
|
||||
To browse the spec without running the server, see [docs/openapi.yaml](./docs/openapi.yaml). For a higher-level overview of how OpenSERP works internally, see the [architecture docs](https://openserp.org/docs/architecture/).
|
||||
|
||||
## SDKs & Integrations
|
||||
## SDKs & Examples
|
||||
|
||||
Official client packages. Each works against your self-hosted server (point it at the server's base URL):
|
||||
Official client packages. Each works against your self-hosted server (set `baseUrl`) or the [hosted API](https://openserp.org/cloud) (set `apiKey`):
|
||||
|
||||
| Type | Package | Install |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
@@ -80,6 +80,8 @@ Official client packages. Each works against your self-hosted server (point it a
|
||||
| MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` |
|
||||
| n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes |
|
||||
|
||||
See [**examples**](./examples) for small JavaScript and Python use cases covering search, AI grounding, SEO, content extraction, and image search.
|
||||
|
||||
```js
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
|
||||
90
examples/README.md
Normal file
90
examples/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# OpenSERP Examples
|
||||
|
||||
Short, copy-pasteable examples for using OpenSERP from JavaScript and Python.
|
||||
Each one runs against a local server by default and works the same against the
|
||||
[hosted API](#using-the-hosted-api) by swapping in an API key.
|
||||
|
||||
## Start a server
|
||||
|
||||
Most examples expect OpenSERP running on `http://localhost:7000`.
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker run -p 127.0.0.1:7000:7000 -it karust/openserp serve -a 0.0.0.0 -p 7000
|
||||
|
||||
# Or from source
|
||||
go build -o openserp . && ./openserp serve
|
||||
```
|
||||
|
||||
Prefer not to run a server? Skip ahead to [Using the hosted API](#using-the-hosted-api).
|
||||
|
||||
## Try it without code
|
||||
|
||||
A search is a single HTTP GET, so you can check the server with `curl`:
|
||||
|
||||
```bash
|
||||
# One engine
|
||||
curl "http://localhost:7000/google/search?text=open+source+search+api&limit=10"
|
||||
|
||||
# Several engines at once
|
||||
curl "http://localhost:7000/mega/search?text=open+source+search+api&engines=bing,duckduckgo&limit=10"
|
||||
```
|
||||
|
||||
## SDKs and integrations
|
||||
|
||||
| Tool | Package | Install |
|
||||
| --- | --- | --- |
|
||||
| JavaScript / TypeScript | [`@openserp/sdk`](https://www.npmjs.com/package/@openserp/sdk) | `npm install @openserp/sdk` |
|
||||
| Python | [`openserp`](https://pypi.org/project/openserp/) | `pip install openserp` |
|
||||
| MCP server (AI agents) | [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp) | `npx @openserp/mcp` |
|
||||
| n8n community node | [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp) | Install via n8n community nodes |
|
||||
|
||||
## Examples by question
|
||||
|
||||
### Getting started
|
||||
|
||||
- **How do I run a search from code?** — [JavaScript](quickstart/js-basic-search) · [Python](quickstart/python-basic-search)
|
||||
- **How do I compare results across several engines?** — [JavaScript](search/js-multi-engine-compare)
|
||||
- **How do I search a list of keywords and export them?** — [Python](search/python-keyword-csv)
|
||||
|
||||
### AI and LLM grounding
|
||||
|
||||
- **How do I ground an LLM answer in fresh search results?** — [JavaScript](ai/js-rag-context-builder)
|
||||
- **How do I give a search tool to an agent?** — [Python](ai/python-agent-tool)
|
||||
- **I want this inside Claude, Cursor, or another MCP client.** — [`@openserp/mcp`](https://www.npmjs.com/package/@openserp/mcp)
|
||||
|
||||
### SEO
|
||||
|
||||
- **Who are my real competitors for a set of keywords?** — [JavaScript](seo/js-competitor-overlap)
|
||||
- **Where does my domain rank for each keyword?** — [Python](seo/python-rank-tracker)
|
||||
|
||||
### Content extraction
|
||||
|
||||
- **How do I search and read the page content in one call?** — [JavaScript](content/js-search-with-extract)
|
||||
- **How do I turn a URL into clean Markdown?** — [Python](content/python-extract-markdown)
|
||||
|
||||
### Images
|
||||
|
||||
- **How do I search images and preview them?** — [JavaScript](media/js-image-gallery)
|
||||
|
||||
### Automation
|
||||
|
||||
- **I want to wire OpenSERP into a no-code workflow.** — [`@openserp/n8n-nodes-openserp`](https://www.npmjs.com/package/@openserp/n8n-nodes-openserp)
|
||||
|
||||
## Using the hosted API
|
||||
|
||||
Every example points at a local server. To use the managed API instead, get a key
|
||||
from [openserp.org/dashboard/keys](https://openserp.org/dashboard/keys) and construct
|
||||
the client with it — no base URL needed:
|
||||
|
||||
```js
|
||||
const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
```
|
||||
|
||||
```python
|
||||
client = OpenSERP(api_key="<YOUR_API_TOKEN>")
|
||||
```
|
||||
|
||||
The endpoints and response shape are identical, so example code moves between
|
||||
self-hosted and hosted by changing only that one line. Set **either** `baseUrl`
|
||||
(self-hosted) **or** `apiKey` (hosted) — not both.
|
||||
11
examples/ai/js-rag-context-builder/README.md
Normal file
11
examples/ai/js-rag-context-builder/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# RAG context builder (JavaScript)
|
||||
|
||||
Searches for a question and formats the top results into a ready-to-use prompt
|
||||
with numbered, citable sources — the retrieval step of a RAG pipeline.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `question` in [index.js](index.js), then pass the printed prompt to your LLM.
|
||||
27
examples/ai/js-rag-context-builder/index.js
Normal file
27
examples/ai/js-rag-context-builder/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000" });
|
||||
|
||||
const question = "What is retrieval augmented generation?";
|
||||
|
||||
const { results } = await client.search({
|
||||
engine: "google",
|
||||
text: question,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Format the top results as numbered, citable context to drop into an LLM prompt.
|
||||
const context = results
|
||||
.map((r, i) => `[${i + 1}] ${r.title}\n${r.url}\n${r.snippet ?? ""}`)
|
||||
.join("\n\n");
|
||||
|
||||
const prompt = `Answer the question using only the search results below. Cite sources as [n].
|
||||
|
||||
Question: ${question}
|
||||
|
||||
Search results:
|
||||
${context}`;
|
||||
|
||||
console.log(prompt);
|
||||
14
examples/ai/js-rag-context-builder/package.json
Normal file
14
examples/ai/js-rag-context-builder/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-rag-context-builder",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
11
examples/ai/python-agent-tool/README.md
Normal file
11
examples/ai/python-agent-tool/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Agent search tool (Python)
|
||||
|
||||
A `web_search(query)` function shaped to register as a tool for an LLM agent.
|
||||
It returns compact JSON-serializable results the model can read and cite.
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Import `web_search` from [main.py](main.py) and register it with your agent framework.
|
||||
29
examples/ai/python-agent-tool/main.py
Normal file
29
examples/ai/python-agent-tool/main.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
|
||||
from openserp import OpenSERP
|
||||
|
||||
# A search function shaped for use as an LLM / agent tool: it takes a query
|
||||
# and returns plain JSON-serializable results the model can read.
|
||||
|
||||
|
||||
def web_search(query: str, limit: int = 10) -> list[dict]:
|
||||
"""Search the web and return a compact list of results."""
|
||||
# Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
# with OpenSERP(api_key="<YOUR_API_TOKEN>") as client:
|
||||
with OpenSERP(base_url="http://localhost:7000") as client:
|
||||
response = client.search(engine="google", text=query, limit=limit)
|
||||
return [
|
||||
{
|
||||
"rank": item.rank,
|
||||
"title": item.title,
|
||||
"url": item.url,
|
||||
"snippet": item.snippet,
|
||||
}
|
||||
for item in response.results
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Register web_search as a tool with your LLM; here we just call it directly.
|
||||
results = web_search("what is retrieval augmented generation")
|
||||
print(json.dumps(results, indent=2))
|
||||
1
examples/ai/python-agent-tool/requirements.txt
Normal file
1
examples/ai/python-agent-tool/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
openserp>=0.2.0,<1
|
||||
11
examples/content/js-search-with-extract/README.md
Normal file
11
examples/content/js-search-with-extract/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Search with extraction (JavaScript)
|
||||
|
||||
Runs a search with `extract: true`, so OpenSERP fetches the top pages and returns
|
||||
their cleaned content alongside each result — search and scrape in one request.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `text`, `extractTop`, or `extractMode` in [index.js](index.js) to tune it.
|
||||
29
examples/content/js-search-with-extract/index.js
Normal file
29
examples/content/js-search-with-extract/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>", timeoutMs: 60_000 });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000", timeoutMs: 60_000 });
|
||||
|
||||
// `extract: true` fetches the top pages and returns their cleaned content
|
||||
// alongside each result, so you get the page text in a single request.
|
||||
// `extractTop` (max 5) controls how many results are enriched.
|
||||
const { results } = await client.search({
|
||||
engine: "ecosia",
|
||||
text: "what is a serp api",
|
||||
extract: true,
|
||||
extractTop: 2,
|
||||
extractMode: "auto",
|
||||
});
|
||||
|
||||
for (const item of results) {
|
||||
console.log(`${item.rank}. ${item.title}`);
|
||||
console.log(` ${item.url}`);
|
||||
|
||||
// When extracted, `item.extracted.content` holds the page body. Trim it to
|
||||
// a short preview here.
|
||||
const content = item.extracted?.content;
|
||||
if (content) {
|
||||
console.log(` ${content.slice(0, 300).trim()}…`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
14
examples/content/js-search-with-extract/package.json
Normal file
14
examples/content/js-search-with-extract/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-search-with-extract",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
10
examples/content/python-extract-markdown/README.md
Normal file
10
examples/content/python-extract-markdown/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Extract a page to Markdown (Python)
|
||||
|
||||
Extracts one URL with `/extract` and writes the cleaned page content to `extracted.md`.
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Edit the `url` in [main.py](main.py) to extract a different page.
|
||||
16
examples/content/python-extract-markdown/main.py
Normal file
16
examples/content/python-extract-markdown/main.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from openserp import OpenSERP
|
||||
|
||||
url = "https://go.dev/doc/"
|
||||
output_file = "extracted.md"
|
||||
|
||||
# Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
# with OpenSERP(api_key="<YOUR_API_TOKEN>", timeout=60.0) as client:
|
||||
with OpenSERP(base_url="http://localhost:7000", timeout=60.0) as client:
|
||||
# /extract turns a single page into clean Markdown (or plain text).
|
||||
result = client.extract(url=url, mode="auto", clean=True)
|
||||
|
||||
content = result.markdown or result.text or ""
|
||||
with open(output_file, "w", encoding="utf-8") as handle:
|
||||
handle.write(content)
|
||||
|
||||
print(f"Extracted {len(content)} characters from {url} into {output_file}")
|
||||
@@ -0,0 +1 @@
|
||||
openserp>=0.2.0,<1
|
||||
10
examples/media/js-image-gallery/README.md
Normal file
10
examples/media/js-image-gallery/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Image gallery (JavaScript)
|
||||
|
||||
Runs an image search and writes a self-contained `gallery.html` you can open in a browser.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `query` in [index.js](index.js), then open `gallery.html`.
|
||||
56
examples/media/js-image-gallery/index.js
Normal file
56
examples/media/js-image-gallery/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000" });
|
||||
|
||||
const query = "go gopher mascot";
|
||||
|
||||
const { results } = await client.image({
|
||||
engine: "bing",
|
||||
text: query,
|
||||
limit: 12,
|
||||
});
|
||||
|
||||
const cards = results
|
||||
.map((result) => {
|
||||
const src = escapeHtml(result.image?.thumbnail ?? result.image?.url ?? "");
|
||||
const title = escapeHtml(result.title ?? "Untitled");
|
||||
const page = escapeHtml(result.source?.page_url ?? "#");
|
||||
return `<a class="card" href="${page}"><img src="${src}" alt="${title}"><span>${title}</span></a>`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${escapeHtml(query)}</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 32px; color: #17202a; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; }
|
||||
.card { color: inherit; text-decoration: none; border: 1px solid #d7dde5; border-radius: 8px; overflow: hidden; }
|
||||
img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: #f2f4f7; display: block; }
|
||||
span { display: block; padding: 10px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(query)}</h1>
|
||||
<div class="grid">${cards}</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
await writeFile("gallery.html", html, "utf8");
|
||||
console.log(`Wrote ${results.length} images to gallery.html — open it in a browser.`);
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>"']/g, (char) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[char]);
|
||||
}
|
||||
14
examples/media/js-image-gallery/package.json
Normal file
14
examples/media/js-image-gallery/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-image-gallery",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
11
examples/quickstart/js-basic-search/README.md
Normal file
11
examples/quickstart/js-basic-search/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Basic search (JavaScript)
|
||||
|
||||
Runs one Google search with `@openserp/sdk` and prints the titles and URLs.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `text`, `engine`, or `limit` in [index.js](index.js) to change the search.
|
||||
To use the hosted API instead of a local server, follow the commented line at the top of the file.
|
||||
18
examples/quickstart/js-basic-search/index.js
Normal file
18
examples/quickstart/js-basic-search/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000" });
|
||||
|
||||
const { results } = await client.search({
|
||||
engine: "google",
|
||||
text: "open source search api",
|
||||
region: "US",
|
||||
lang: "EN",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
for (const item of results) {
|
||||
console.log(`${item.rank}. ${item.title}`);
|
||||
console.log(` ${item.url}`);
|
||||
}
|
||||
14
examples/quickstart/js-basic-search/package.json
Normal file
14
examples/quickstart/js-basic-search/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-basic-search",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
11
examples/quickstart/python-basic-search/README.md
Normal file
11
examples/quickstart/python-basic-search/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Basic search (Python)
|
||||
|
||||
Runs one Google search with the `openserp` SDK and prints the titles and URLs.
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Edit the `text`, `engine`, or `limit` in [main.py](main.py) to change the search.
|
||||
To use the hosted API instead of a local server, follow the commented line at the top of the file.
|
||||
16
examples/quickstart/python-basic-search/main.py
Normal file
16
examples/quickstart/python-basic-search/main.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from openserp import OpenSERP
|
||||
|
||||
# Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
# with OpenSERP(api_key="<YOUR_API_TOKEN>") as client:
|
||||
with OpenSERP(base_url="http://localhost:7000") as client:
|
||||
response = client.search(
|
||||
engine="google",
|
||||
text="open source search api",
|
||||
region="US",
|
||||
lang="EN",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
for item in response.results:
|
||||
print(f"{item.rank}. {item.title}")
|
||||
print(f" {item.url}")
|
||||
1
examples/quickstart/python-basic-search/requirements.txt
Normal file
1
examples/quickstart/python-basic-search/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
openserp>=0.2.0,<1
|
||||
11
examples/search/js-multi-engine-compare/README.md
Normal file
11
examples/search/js-multi-engine-compare/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Multi-engine compare (JavaScript)
|
||||
|
||||
Runs one query across several engines with `/mega/search` and groups the results
|
||||
by domain, so you can see which sites the engines agree on.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `query` and `engines` in [index.js](index.js) to compare your own.
|
||||
34
examples/search/js-multi-engine-compare/index.js
Normal file
34
examples/search/js-multi-engine-compare/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000" });
|
||||
|
||||
const query = "privacy focused search engine";
|
||||
|
||||
// /mega/search runs one query across several engines and merges the results.
|
||||
const { results } = await client.megaSearch({
|
||||
text: query,
|
||||
engines: ["bing", "duckduckgo"],
|
||||
region: "US",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// Group results by domain to see which sites both engines agree on.
|
||||
const byDomain = new Map();
|
||||
for (const result of results) {
|
||||
const domain = result.domain;
|
||||
if (!domain) continue;
|
||||
|
||||
const stats = byDomain.get(domain) ?? { hits: 0, engines: new Set(), bestRank: Infinity };
|
||||
stats.hits += 1;
|
||||
stats.engines.add(result.engine);
|
||||
stats.bestRank = Math.min(stats.bestRank, result.rank ?? Infinity);
|
||||
byDomain.set(domain, stats);
|
||||
}
|
||||
|
||||
console.log(`Domains found for "${query}":\n`);
|
||||
const ranked = [...byDomain].sort((a, b) => a[1].bestRank - b[1].bestRank);
|
||||
for (const [domain, stats] of ranked) {
|
||||
console.log(`${domain} (rank ${stats.bestRank}, seen in ${[...stats.engines].join(", ")})`);
|
||||
}
|
||||
14
examples/search/js-multi-engine-compare/package.json
Normal file
14
examples/search/js-multi-engine-compare/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-multi-engine-compare",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
10
examples/search/python-keyword-csv/README.md
Normal file
10
examples/search/python-keyword-csv/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Keyword list to CSV (Python)
|
||||
|
||||
Searches a list of keywords and writes every result to `keywords.csv`.
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Edit the `keywords` list in [main.py](main.py) to export your own.
|
||||
31
examples/search/python-keyword-csv/main.py
Normal file
31
examples/search/python-keyword-csv/main.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import csv
|
||||
|
||||
from openserp import OpenSERP
|
||||
|
||||
|
||||
keywords = ["search api", "open source serp"]
|
||||
output_file = "keywords.csv"
|
||||
|
||||
# Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
#with OpenSERP(api_key="<YOUR_API_TOKEN>") as client:
|
||||
with OpenSERP(base_url="http://localhost:7000") as client:
|
||||
rows = []
|
||||
for keyword in keywords:
|
||||
response = client.search(engine="google", text=keyword, region="US", limit=10)
|
||||
for result in response.results:
|
||||
rows.append(
|
||||
{
|
||||
"keyword": keyword,
|
||||
"rank": result.rank,
|
||||
"title": result.title,
|
||||
"url": result.url,
|
||||
"domain": result.domain,
|
||||
}
|
||||
)
|
||||
|
||||
with open(output_file, "w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=["keyword", "rank", "title", "url", "domain"])
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"Wrote {len(rows)} rows to {output_file}")
|
||||
1
examples/search/python-keyword-csv/requirements.txt
Normal file
1
examples/search/python-keyword-csv/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
openserp>=0.2.0,<1
|
||||
11
examples/seo/js-competitor-overlap/README.md
Normal file
11
examples/seo/js-competitor-overlap/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Competitor overlap (JavaScript)
|
||||
|
||||
Searches several keywords and ranks the domains by how many of them they appear in —
|
||||
the sites showing up across your keyword set are your real SERP competitors.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
node index.js
|
||||
```
|
||||
|
||||
Edit the `keywords` list in [index.js](index.js) to match your niche.
|
||||
32
examples/seo/js-competitor-overlap/index.js
Normal file
32
examples/seo/js-competitor-overlap/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { OpenSERP } from "@openserp/sdk";
|
||||
|
||||
// Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
// const client = new OpenSERP({ apiKey: "<YOUR_API_TOKEN>" });
|
||||
const client = new OpenSERP({ baseUrl: "http://localhost:7000" });
|
||||
|
||||
// Domains that rank for several of your keywords are your real SERP competitors.
|
||||
const keywords = ["serp api", "google search api", "scrape search results"];
|
||||
|
||||
const byDomain = new Map();
|
||||
for (const keyword of keywords) {
|
||||
const { results } = await client.search({
|
||||
engine: "google",
|
||||
text: keyword,
|
||||
region: "US",
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
if (!result.domain) continue;
|
||||
const stats = byDomain.get(result.domain) ?? { keywords: new Set(), bestRank: Infinity };
|
||||
stats.keywords.add(keyword);
|
||||
stats.bestRank = Math.min(stats.bestRank, result.rank ?? Infinity);
|
||||
byDomain.set(result.domain, stats);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Competitor overlap across ${keywords.length} keywords:\n`);
|
||||
const ranked = [...byDomain].sort((a, b) => b[1].keywords.size - a[1].keywords.size);
|
||||
for (const [domain, stats] of ranked) {
|
||||
console.log(`${domain} — ${stats.keywords.size}/${keywords.length} keywords, best rank ${stats.bestRank}`);
|
||||
}
|
||||
14
examples/seo/js-competitor-overlap/package.json
Normal file
14
examples/seo/js-competitor-overlap/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "openserp-example-js-competitor-overlap",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openserp/sdk": "^0.2.0"
|
||||
}
|
||||
}
|
||||
11
examples/seo/python-rank-tracker/README.md
Normal file
11
examples/seo/python-rank-tracker/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Rank tracker (Python)
|
||||
|
||||
Checks where a target domain ranks for a list of keywords and writes the results
|
||||
to `rankings.csv`. Handles `www.` and subdomains when matching the domain.
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Edit `target_domain` and `keywords` in [main.py](main.py) to track your own site.
|
||||
38
examples/seo/python-rank-tracker/main.py
Normal file
38
examples/seo/python-rank-tracker/main.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import csv
|
||||
|
||||
from openserp import OpenSERP
|
||||
|
||||
target_domain = "go.dev"
|
||||
keywords = ["golang tutorial", "go programming language", "golang documentation"]
|
||||
output_file = "rankings.csv"
|
||||
|
||||
|
||||
def matches(domain: str | None, target: str) -> bool:
|
||||
if not domain:
|
||||
return False
|
||||
domain = domain.lower().removeprefix("www.")
|
||||
return domain == target or domain.endswith(f".{target}")
|
||||
|
||||
|
||||
# Hosted API instead? Get a key at https://openserp.org/dashboard/keys:
|
||||
# with OpenSERP(api_key="<YOUR_API_TOKEN>") as client:
|
||||
with OpenSERP(base_url="http://localhost:7000") as client:
|
||||
rows = []
|
||||
for keyword in keywords:
|
||||
response = client.search(engine="google", text=keyword, region="US", limit=20)
|
||||
hit = next((r for r in response.results if matches(r.domain, target_domain)), None)
|
||||
rows.append(
|
||||
{
|
||||
"keyword": keyword,
|
||||
"rank": hit.rank if hit else "not in top 20",
|
||||
"url": hit.url if hit else "",
|
||||
}
|
||||
)
|
||||
print(f'"{keyword}": {rows[-1]["rank"]}')
|
||||
|
||||
with open(output_file, "w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=["keyword", "rank", "url"])
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"\nSaved rankings for {target_domain} to {output_file}")
|
||||
1
examples/seo/python-rank-tracker/requirements.txt
Normal file
1
examples/seo/python-rank-tracker/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
openserp>=0.2.0,<1
|
||||
Reference in New Issue
Block a user