search-web-api 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +307 -0
- package/demo/index.ts +38 -0
- package/demo/openapi.ts +402 -0
- package/demo/routes/autocomplete.ts +124 -0
- package/demo/routes/search.ts +61 -0
- package/docs/AUTOCOMPLETE.md +296 -0
- package/docs/CATEGORY_SEARCH.md +265 -0
- package/package.json +32 -0
- package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
- package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
- package/src/category-registry.ts +6 -0
- package/src/config/search-engine-constants.ts +216 -0
- package/src/constants.ts +6 -0
- package/src/engine-descriptions.ts +7 -0
- package/src/engine-status.ts +7 -0
- package/src/engine.ts +6 -0
- package/src/registry/search-engine-category-registry.ts +200 -0
- package/src/registry/search-engine-descriptions.ts +121 -0
- package/src/registry/search-engine-status-tracker.ts +240 -0
- package/src/result-container.ts +8 -0
- package/src/search/search-engines-registry-list.ts +205 -0
- package/src/search/search-query-executor.ts +168 -0
- package/src/search/search-result-container.ts +421 -0
- package/src/search-web-types.ts +7 -0
- package/src/search.ts +10 -0
- package/src/sources/academic/arxiv.ts +72 -0
- package/src/sources/academic/core.ts +65 -0
- package/src/sources/academic/crossref.ts +75 -0
- package/src/sources/academic/doaj.ts +63 -0
- package/src/sources/academic/google_scholar.ts +53 -0
- package/src/sources/academic/openalex.ts +75 -0
- package/src/sources/academic/pubmed.ts +96 -0
- package/src/sources/academic/semantic_scholar.ts +77 -0
- package/src/sources/academic/wikidata.ts +44 -0
- package/src/sources/general/baidu.ts +59 -0
- package/src/sources/general/bing.ts +30 -0
- package/src/sources/general/brave.ts +54 -0
- package/src/sources/general/duckduckgo.ts +49 -0
- package/src/sources/general/google.ts +68 -0
- package/src/sources/general/mojeek.ts +56 -0
- package/src/sources/general/qwant.ts +37 -0
- package/src/sources/general/startpage.ts +44 -0
- package/src/sources/general/yahoo.ts +41 -0
- package/src/sources/general/yandex.ts +56 -0
- package/src/sources/images/bing_images.ts +68 -0
- package/src/sources/images/deviantart.ts +71 -0
- package/src/sources/images/flickr.ts +132 -0
- package/src/sources/images/google_images.ts +101 -0
- package/src/sources/images/imgur.ts +58 -0
- package/src/sources/images/openclipart.ts +52 -0
- package/src/sources/images/pixabay.ts +56 -0
- package/src/sources/images/unsplash.ts +38 -0
- package/src/sources/images/wallhaven.ts +50 -0
- package/src/sources/it/crates.ts +41 -0
- package/src/sources/it/dockerhub.ts +47 -0
- package/src/sources/it/github.ts +37 -0
- package/src/sources/it/gitlab.ts +55 -0
- package/src/sources/it/npm.ts +36 -0
- package/src/sources/it/packagist.ts +43 -0
- package/src/sources/it/pypi.ts +30 -0
- package/src/sources/it/rubygems.ts +43 -0
- package/src/sources/it/stackoverflow.ts +39 -0
- package/src/sources/maps/apple_maps.ts +105 -0
- package/src/sources/maps/openstreetmap.ts +34 -0
- package/src/sources/maps/photon.ts +77 -0
- package/src/sources/news/bing_news.ts +95 -0
- package/src/sources/news/google_news.ts +80 -0
- package/src/sources/news/hackernews.ts +94 -0
- package/src/sources/news/yahoo_news.ts +77 -0
- package/src/sources/shopping/ebay.ts +96 -0
- package/src/sources/social/mastodon.ts +46 -0
- package/src/sources/social/medium.ts +52 -0
- package/src/sources/social/reddit.ts +48 -0
- package/src/sources/social/soundcloud.ts +64 -0
- package/src/sources/social/twitter.ts +56 -0
- package/src/sources/specialized/annas_archive.ts +97 -0
- package/src/sources/specialized/archive.ts +48 -0
- package/src/sources/specialized/genius.ts +43 -0
- package/src/sources/specialized/goodreads.ts +62 -0
- package/src/sources/specialized/imdb.ts +55 -0
- package/src/sources/specialized/openlibrary.ts +59 -0
- package/src/sources/specialized/wikipedia.ts +37 -0
- package/src/sources/specialized/wttr.ts +98 -0
- package/src/sources/torrents/1337x.ts +43 -0
- package/src/sources/torrents/eztv.ts +47 -0
- package/src/sources/torrents/kickass.ts +63 -0
- package/src/sources/torrents/nyaa.ts +53 -0
- package/src/sources/torrents/solidtorrents.ts +68 -0
- package/src/sources/torrents/thepiratebay.ts +57 -0
- package/src/sources/torrents/yts.ts +54 -0
- package/src/sources/videos/bing_videos.ts +91 -0
- package/src/sources/videos/dailymotion.ts +101 -0
- package/src/sources/videos/invidious.ts +86 -0
- package/src/sources/videos/peertube.ts +76 -0
- package/src/sources/videos/vimeo.ts +67 -0
- package/src/sources/videos/youtube.ts +78 -0
- package/src/suggest-next-words/autocomplete-ai.ts +38 -0
- package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
- package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
- package/src/types/search-engine-interface.ts +27 -0
- package/src/types/search-result-types.ts +405 -0
- package/test/api.test.ts +128 -0
- package/test/autocomplete-ai.test.ts +20 -0
- package/test/autocomplete-engines.test.ts +131 -0
- package/test/engine-health-suite.test.ts +350 -0
- package/test/search.test.ts +69 -0
- package/test/sources-unit.test.ts +1152 -0
- package/test/sources.test.ts +182 -0
- package/test/test-utils.ts +81 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const core: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const apiKey = process.env.CORE_API_KEY;
|
|
8
|
+
if (!apiKey) return [];
|
|
9
|
+
|
|
10
|
+
const limit = 10;
|
|
11
|
+
const offset = ((page || 1) - 1) * limit;
|
|
12
|
+
|
|
13
|
+
const response = await fetch(
|
|
14
|
+
"https://api.core.ac.uk/v3/search/works",
|
|
15
|
+
{
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
"User-Agent":
|
|
21
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({ q: query, limit, offset, scroll: false }),
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!response.ok) return [];
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
|
|
31
|
+
if (!data || !Array.isArray(data.results)) return [];
|
|
32
|
+
|
|
33
|
+
return data.results
|
|
34
|
+
.map((r: any) => {
|
|
35
|
+
const title = r.title || "";
|
|
36
|
+
const doi = r.doi || "";
|
|
37
|
+
const url =
|
|
38
|
+
r.downloadUrl ||
|
|
39
|
+
(doi ? `https://doi.org/${doi}` : "") ||
|
|
40
|
+
(r.id ? `https://core.ac.uk/works/${r.id}` : "");
|
|
41
|
+
const authors = (r.authors || [])
|
|
42
|
+
.slice(0, 3)
|
|
43
|
+
.map((a: any) => a.name)
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
const journal = r.journals?.[0]?.title || "";
|
|
46
|
+
const abstract = (r.abstract || "").slice(0, 500);
|
|
47
|
+
|
|
48
|
+
const content = [
|
|
49
|
+
abstract,
|
|
50
|
+
authors.length
|
|
51
|
+
? `Authors: ${authors.join(", ")}${
|
|
52
|
+
(r.authors || []).length > 3 ? " et al." : ""
|
|
53
|
+
}`
|
|
54
|
+
: "",
|
|
55
|
+
r.yearPublished ? `Year: ${r.yearPublished}` : "",
|
|
56
|
+
journal ? `Journal: ${journal}` : "",
|
|
57
|
+
doi ? `DOI: ${doi}` : "",
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join("\n");
|
|
61
|
+
|
|
62
|
+
return { url, title, content, engine: "core" };
|
|
63
|
+
})
|
|
64
|
+
.filter((r: any) => r.url && r.title);
|
|
65
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const crossref: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
query: query,
|
|
9
|
+
offset: String(20 * ((page || 1) - 1)),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
`https://api.crossref.org/works?${params}`,
|
|
14
|
+
{
|
|
15
|
+
headers: {
|
|
16
|
+
"User-Agent":
|
|
17
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (!response.ok) return [];
|
|
23
|
+
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
|
|
26
|
+
if (!data || !data.message || !data.message.items) return [];
|
|
27
|
+
|
|
28
|
+
return data.message.items
|
|
29
|
+
.filter((record: any) => record.type !== "component")
|
|
30
|
+
.map((record: any) => {
|
|
31
|
+
let title = "";
|
|
32
|
+
let journal = "";
|
|
33
|
+
|
|
34
|
+
if (record.type === "book-chapter") {
|
|
35
|
+
title = record["container-title"]?.[0] || "";
|
|
36
|
+
if (
|
|
37
|
+
record.title?.[0] &&
|
|
38
|
+
record.title[0].toLowerCase().trim() !==
|
|
39
|
+
title.toLowerCase().trim()
|
|
40
|
+
) {
|
|
41
|
+
title += ` (${record.title[0]})`;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
title =
|
|
45
|
+
record.title?.[0] || record["container-title"]?.[0] || "";
|
|
46
|
+
journal =
|
|
47
|
+
record["container-title"]?.[0] && record.title?.[0]
|
|
48
|
+
? record["container-title"][0]
|
|
49
|
+
: "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const authors = (record.author || [])
|
|
53
|
+
.map((a: any) => `${a.given || ""} ${a.family || ""}`.trim())
|
|
54
|
+
.filter((a: string) => a)
|
|
55
|
+
.join(", ");
|
|
56
|
+
|
|
57
|
+
const content = [
|
|
58
|
+
record.abstract || "",
|
|
59
|
+
journal ? `Journal: ${journal}` : "",
|
|
60
|
+
authors ? `Authors: ${authors}` : "",
|
|
61
|
+
record.publisher ? `Publisher: ${record.publisher}` : "",
|
|
62
|
+
record.DOI ? `DOI: ${record.DOI}` : "",
|
|
63
|
+
]
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join("\n");
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
url: record.URL || `https://doi.org/${record.DOI}` || "",
|
|
69
|
+
title,
|
|
70
|
+
content,
|
|
71
|
+
engine: "crossref",
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
.filter((r: any) => r.url && r.title);
|
|
75
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const doaj: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
pageSize: "10",
|
|
9
|
+
page: String(page || 1),
|
|
10
|
+
sort: "created_date:desc",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const response = await fetch(
|
|
14
|
+
`https://doaj.org/api/v2/search/articles/${encodeURIComponent(query)}?${params}`,
|
|
15
|
+
{
|
|
16
|
+
headers: {
|
|
17
|
+
"User-Agent":
|
|
18
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
19
|
+
Accept: "application/json",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!response.ok) return [];
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
if (!data || !Array.isArray(data.results)) return [];
|
|
29
|
+
|
|
30
|
+
return data.results
|
|
31
|
+
.map((r: any) => {
|
|
32
|
+
const bib = r.bibjson || {};
|
|
33
|
+
const title = bib.title || "";
|
|
34
|
+
const doi =
|
|
35
|
+
bib.identifier?.find((i: any) => i.type === "doi")?.id || "";
|
|
36
|
+
const link =
|
|
37
|
+
bib.link?.find((l: any) => l.type === "fulltext")?.url || "";
|
|
38
|
+
const url = link || (doi ? `https://doi.org/${doi}` : "");
|
|
39
|
+
const authors = (bib.author || [])
|
|
40
|
+
.slice(0, 3)
|
|
41
|
+
.map((a: any) => a.name)
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
const journal = bib.journal?.title || "";
|
|
44
|
+
const abstract = (bib.abstract || "").slice(0, 500);
|
|
45
|
+
|
|
46
|
+
const content = [
|
|
47
|
+
abstract,
|
|
48
|
+
authors.length
|
|
49
|
+
? `Authors: ${authors.join(", ")}${
|
|
50
|
+
(bib.author || []).length > 3 ? " et al." : ""
|
|
51
|
+
}`
|
|
52
|
+
: "",
|
|
53
|
+
bib.year ? `Year: ${bib.year}` : "",
|
|
54
|
+
journal ? `Journal: ${journal}` : "",
|
|
55
|
+
doi ? `DOI: ${doi}` : "",
|
|
56
|
+
]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join("\n");
|
|
59
|
+
|
|
60
|
+
return { url, title, content, engine: "doaj" };
|
|
61
|
+
})
|
|
62
|
+
.filter((r: any) => r.url && r.title);
|
|
63
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const google_scholar: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
start: String(((page || 1) - 1) * 10),
|
|
11
|
+
hl: "en",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const response = await fetch(
|
|
15
|
+
`https://scholar.google.com/scholar?${params}`,
|
|
16
|
+
{
|
|
17
|
+
headers: {
|
|
18
|
+
"User-Agent":
|
|
19
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
20
|
+
Accept:
|
|
21
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
22
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
23
|
+
Cookie: "CONSENT=YES+",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!response.ok) return [];
|
|
29
|
+
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
const { document } = parseHTML(text);
|
|
32
|
+
|
|
33
|
+
return Array.from(document.querySelectorAll(".gs_r.gs_or.gs_scl"))
|
|
34
|
+
.map((element) => {
|
|
35
|
+
const titleLink = element.querySelector(".gs_rt a");
|
|
36
|
+
const url = titleLink?.getAttribute("href") || "";
|
|
37
|
+
const title = titleLink?.textContent?.trim() || "";
|
|
38
|
+
const content =
|
|
39
|
+
element.querySelector(".gs_rs")?.textContent?.trim() || "";
|
|
40
|
+
const publicationInfo =
|
|
41
|
+
element.querySelector(".gs_a")?.textContent?.trim() || "";
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
url,
|
|
45
|
+
title,
|
|
46
|
+
content: publicationInfo
|
|
47
|
+
? `${publicationInfo} - ${content}`
|
|
48
|
+
: content,
|
|
49
|
+
engine: "google_scholar",
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.filter((r) => r.url && r.title);
|
|
53
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
function reconstructAbstract(
|
|
4
|
+
invertedIndex: Record<string, number[]>
|
|
5
|
+
): string {
|
|
6
|
+
const words: string[] = [];
|
|
7
|
+
for (const [word, positions] of Object.entries(invertedIndex)) {
|
|
8
|
+
for (const pos of positions) words[pos] = word;
|
|
9
|
+
}
|
|
10
|
+
return words.filter(Boolean).join(" ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const openalex: EngineFunction = async (
|
|
14
|
+
query: string,
|
|
15
|
+
page: number | undefined
|
|
16
|
+
) => {
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
search: query,
|
|
19
|
+
"per-page": "10",
|
|
20
|
+
page: String(page || 1),
|
|
21
|
+
sort: "cited_by_count:desc",
|
|
22
|
+
mailto: process.env.OPENALEX_MAILTO || "research@qwksearch.com",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const response = await fetch(
|
|
26
|
+
`https://api.openalex.org/works?${params}`,
|
|
27
|
+
{
|
|
28
|
+
headers: {
|
|
29
|
+
"User-Agent":
|
|
30
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!response.ok) return [];
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
|
|
40
|
+
if (!data || !Array.isArray(data.results)) return [];
|
|
41
|
+
|
|
42
|
+
return data.results
|
|
43
|
+
.map((w: any) => {
|
|
44
|
+
const title = w.title || "";
|
|
45
|
+
const doi = w.doi || "";
|
|
46
|
+
const url =
|
|
47
|
+
w.primary_location?.landing_page_url || doi || w.id || "";
|
|
48
|
+
const authors = (w.authorships || [])
|
|
49
|
+
.slice(0, 3)
|
|
50
|
+
.map((a: any) => a.author?.display_name)
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const abstract = w.abstract_inverted_index
|
|
53
|
+
? reconstructAbstract(w.abstract_inverted_index).slice(0, 500)
|
|
54
|
+
: "";
|
|
55
|
+
const journal = w.host_venue?.display_name || "";
|
|
56
|
+
|
|
57
|
+
const content = [
|
|
58
|
+
abstract,
|
|
59
|
+
authors.length
|
|
60
|
+
? `Authors: ${authors.join(", ")}${
|
|
61
|
+
(w.authorships || []).length > 3 ? " et al." : ""
|
|
62
|
+
}`
|
|
63
|
+
: "",
|
|
64
|
+
w.publication_year ? `Year: ${w.publication_year}` : "",
|
|
65
|
+
journal ? `Journal: ${journal}` : "",
|
|
66
|
+
w.cited_by_count ? `Citations: ${w.cited_by_count}` : "",
|
|
67
|
+
doi ? `DOI: ${doi}` : "",
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
|
|
72
|
+
return { url, title, content, engine: "openalex" };
|
|
73
|
+
})
|
|
74
|
+
.filter((r: any) => r.url && r.title);
|
|
75
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const pubmed: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const pageno = page || 1;
|
|
9
|
+
const number_of_results = 10;
|
|
10
|
+
const retstart = (pageno - 1) * number_of_results;
|
|
11
|
+
|
|
12
|
+
const esearchParams = new URLSearchParams({
|
|
13
|
+
db: "pubmed",
|
|
14
|
+
term: query,
|
|
15
|
+
retstart: String(retstart),
|
|
16
|
+
retmax: String(number_of_results),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const esearchResponse = await fetch(
|
|
20
|
+
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?${esearchParams}`,
|
|
21
|
+
{
|
|
22
|
+
headers: {
|
|
23
|
+
"User-Agent":
|
|
24
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!esearchResponse.ok) return [];
|
|
30
|
+
|
|
31
|
+
const esearchText = await esearchResponse.text();
|
|
32
|
+
const { document: esearchDoc } = parseHTML(esearchText);
|
|
33
|
+
const pmids: string[] = [];
|
|
34
|
+
esearchDoc.querySelectorAll("Id").forEach((elem) => {
|
|
35
|
+
const pmid = elem.textContent;
|
|
36
|
+
if (pmid) pmids.push(pmid);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (pmids.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
const efetchResponse = await fetch(
|
|
42
|
+
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id=${pmids.join(",")}`,
|
|
43
|
+
{
|
|
44
|
+
headers: {
|
|
45
|
+
"User-Agent":
|
|
46
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!efetchResponse.ok) return [];
|
|
52
|
+
|
|
53
|
+
const efetchText = await efetchResponse.text();
|
|
54
|
+
const { document } = parseHTML(efetchText);
|
|
55
|
+
|
|
56
|
+
return Array.from(document.querySelectorAll("PubmedArticle"))
|
|
57
|
+
.map((article) => {
|
|
58
|
+
const title =
|
|
59
|
+
article.querySelector("ArticleTitle")?.textContent || "";
|
|
60
|
+
const pmid = article.querySelector("PMID")?.textContent || "";
|
|
61
|
+
const url = `https://www.ncbi.nlm.nih.gov/pubmed/${pmid}`;
|
|
62
|
+
|
|
63
|
+
const abstractElements = article.querySelectorAll("AbstractText");
|
|
64
|
+
const abstract = Array.from(abstractElements)
|
|
65
|
+
.map((el) => el.textContent)
|
|
66
|
+
.join(" ");
|
|
67
|
+
|
|
68
|
+
const journal =
|
|
69
|
+
article.querySelector("Journal Title")?.textContent || "";
|
|
70
|
+
const doi =
|
|
71
|
+
article.querySelector('ELocationID[EIdType="doi"]')
|
|
72
|
+
?.textContent || "";
|
|
73
|
+
|
|
74
|
+
const authors: string[] = [];
|
|
75
|
+
article.querySelectorAll("AuthorList Author").forEach((author) => {
|
|
76
|
+
const firstName =
|
|
77
|
+
author.querySelector("ForeName")?.textContent || "";
|
|
78
|
+
const lastName =
|
|
79
|
+
author.querySelector("LastName")?.textContent || "";
|
|
80
|
+
const authorName = `${firstName} ${lastName}`.trim();
|
|
81
|
+
if (authorName) authors.push(authorName);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const content = [
|
|
85
|
+
abstract,
|
|
86
|
+
journal ? `Journal: ${journal}` : "",
|
|
87
|
+
authors.length ? `Authors: ${authors.join(", ")}` : "",
|
|
88
|
+
doi ? `DOI: ${doi}` : "",
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join("\n");
|
|
92
|
+
|
|
93
|
+
return { url, title, content, engine: "pubmed" };
|
|
94
|
+
})
|
|
95
|
+
.filter((r) => r.url && r.title);
|
|
96
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const semantic_scholar: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const response = await fetch(
|
|
8
|
+
"https://www.semanticscholar.org/api/1/search",
|
|
9
|
+
{
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
"User-Agent":
|
|
13
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
queryString: query,
|
|
18
|
+
page: page || 1,
|
|
19
|
+
pageSize: 10,
|
|
20
|
+
sort: "relevance",
|
|
21
|
+
getQuerySuggestions: false,
|
|
22
|
+
authors: [],
|
|
23
|
+
coAuthors: [],
|
|
24
|
+
venues: [],
|
|
25
|
+
performTitleMatch: true,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!response.ok) return [];
|
|
31
|
+
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
|
|
34
|
+
if (!data.results) return [];
|
|
35
|
+
|
|
36
|
+
return data.results.map((result: any) => {
|
|
37
|
+
let url = result.primaryPaperLink?.url;
|
|
38
|
+
if (!url && result.links && result.links.length > 0) {
|
|
39
|
+
url = result.links[0];
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!url &&
|
|
43
|
+
result.alternatePaperLinks &&
|
|
44
|
+
result.alternatePaperLinks.length > 0
|
|
45
|
+
) {
|
|
46
|
+
url = result.alternatePaperLinks[0].url;
|
|
47
|
+
}
|
|
48
|
+
if (!url) {
|
|
49
|
+
url = `https://www.semanticscholar.org/paper/${result.id}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const title = result.title?.text || "";
|
|
53
|
+
const abstract = result.abstract?.text || "";
|
|
54
|
+
|
|
55
|
+
const metadata: string[] = [];
|
|
56
|
+
if (result.authors && result.authors.length > 0) {
|
|
57
|
+
const authorNames = result.authors
|
|
58
|
+
.map((a: any) => a.name)
|
|
59
|
+
.slice(0, 3);
|
|
60
|
+
metadata.push(
|
|
61
|
+
`Authors: ${authorNames.join(", ")}${
|
|
62
|
+
result.authors.length > 3 ? " et al." : ""
|
|
63
|
+
}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (result.year) metadata.push(`Year: ${result.year}`);
|
|
67
|
+
if (result.venue) metadata.push(`Venue: ${result.venue}`);
|
|
68
|
+
if (result.citationCount) metadata.push(`Citations: ${result.citationCount}`);
|
|
69
|
+
|
|
70
|
+
const content =
|
|
71
|
+
metadata.length > 0
|
|
72
|
+
? `${metadata.join(" | ")}\n\n${abstract}`
|
|
73
|
+
: abstract;
|
|
74
|
+
|
|
75
|
+
return { url, title, content, engine: "semantic_scholar" };
|
|
76
|
+
});
|
|
77
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const wikidata: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
_page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
action: "wbsearchentities",
|
|
9
|
+
search: query,
|
|
10
|
+
language: "en",
|
|
11
|
+
format: "json",
|
|
12
|
+
limit: "20",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const response = await fetch(
|
|
16
|
+
`https://www.wikidata.org/w/api.php?${params}`,
|
|
17
|
+
{
|
|
18
|
+
headers: {
|
|
19
|
+
"User-Agent":
|
|
20
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (!response.ok) return [];
|
|
26
|
+
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
|
|
29
|
+
if (!data || !data.search) return [];
|
|
30
|
+
|
|
31
|
+
return data.search.map((item: any) => {
|
|
32
|
+
const id = item.id;
|
|
33
|
+
const title = item.label || id;
|
|
34
|
+
const description = item.description || "No description available";
|
|
35
|
+
const url = item.url || `https://www.wikidata.org/wiki/${id}`;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
url,
|
|
39
|
+
title,
|
|
40
|
+
content: description,
|
|
41
|
+
engine: "wikidata",
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const baidu: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
wd: query,
|
|
10
|
+
rn: "10",
|
|
11
|
+
pn: String(((page || 1) - 1) * 10),
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(`https://www.baidu.com/s?${params}`, {
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
16
|
+
Accept:
|
|
17
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
19
|
+
Cookie: "BAIDUID=1234567890:FG=1",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) return [];
|
|
23
|
+
const htmlString = await response.text();
|
|
24
|
+
const { document } = parseHTML(htmlString);
|
|
25
|
+
|
|
26
|
+
return Array.from(
|
|
27
|
+
document.querySelectorAll(
|
|
28
|
+
"#content_left > div.result, div.c-container"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
.map((element) => {
|
|
32
|
+
const titleLink = element.querySelector("h3 a, h3.c-title a");
|
|
33
|
+
const url = titleLink?.getAttribute("href") || "";
|
|
34
|
+
const title = titleLink?.textContent?.trim() || "";
|
|
35
|
+
|
|
36
|
+
if (!url || !title) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content =
|
|
41
|
+
element
|
|
42
|
+
.querySelector(
|
|
43
|
+
"div.c-abstract, div.c-span9, span.content-right_2s-H4"
|
|
44
|
+
)
|
|
45
|
+
?.textContent?.trim() || "";
|
|
46
|
+
const timestamp =
|
|
47
|
+
element
|
|
48
|
+
.querySelector("span.c-color-gray2, span.newTimeFactor_2s-H4")
|
|
49
|
+
?.textContent?.trim() || "";
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
url,
|
|
53
|
+
title,
|
|
54
|
+
content: timestamp ? `${timestamp}\n${content}` : content,
|
|
55
|
+
engine: "baidu",
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
.filter((r) => r !== null && r.url && r.title);
|
|
59
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const bing: EngineFunction = async (q: string, page?: number) => {
|
|
5
|
+
const params = new URLSearchParams({
|
|
6
|
+
q,
|
|
7
|
+
first: String(((page || 1) - 1) * 10 + 1),
|
|
8
|
+
});
|
|
9
|
+
const response = await fetch(`https://www.bing.com/search?${params}`, {
|
|
10
|
+
headers: {
|
|
11
|
+
"User-Agent":
|
|
12
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
13
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
14
|
+
Cookie: "CONSENT=YES+",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) return [];
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
const { document } = parseHTML(text);
|
|
20
|
+
return Array.from(document.querySelectorAll("li.b_algo"))
|
|
21
|
+
.map((el) => {
|
|
22
|
+
return {
|
|
23
|
+
url: el.querySelector("h2 a")?.getAttribute("href") || "",
|
|
24
|
+
title: el.querySelector("h2 a")?.textContent?.trim() || "",
|
|
25
|
+
content: el.querySelector(".b_caption p")?.textContent?.trim() || "",
|
|
26
|
+
engine: "bing",
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.filter((r) => r.url && r.title);
|
|
30
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const brave: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
p: String(page || 1),
|
|
11
|
+
});
|
|
12
|
+
const response = await fetch(`https://search.brave.com/search?${params}`, {
|
|
13
|
+
headers: {
|
|
14
|
+
"User-Agent":
|
|
15
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
16
|
+
Accept:
|
|
17
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const htmlString = await response.text();
|
|
23
|
+
const { document } = parseHTML(htmlString);
|
|
24
|
+
|
|
25
|
+
const snippets = document.querySelectorAll(".snippet");
|
|
26
|
+
console.log(`Brave: Found ${snippets.length} snippets`);
|
|
27
|
+
|
|
28
|
+
const data = Array.from(snippets)
|
|
29
|
+
.map((element) => {
|
|
30
|
+
const link = element.querySelector("a");
|
|
31
|
+
const url = link?.getAttribute("href") || "";
|
|
32
|
+
const title =
|
|
33
|
+
element.querySelector(".search-snippet-title")?.textContent?.trim() || "";
|
|
34
|
+
const content =
|
|
35
|
+
element
|
|
36
|
+
.querySelector(".content")
|
|
37
|
+
?.textContent?.trim() || "";
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
url,
|
|
41
|
+
title,
|
|
42
|
+
content,
|
|
43
|
+
engine: "brave",
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
.filter((r) => r.url && r.title);
|
|
47
|
+
|
|
48
|
+
console.log(`Brave: Extracted ${data.length} valid results`);
|
|
49
|
+
if (data.length === 0 && htmlString.length > 0) {
|
|
50
|
+
console.log("Brave: HTML sample:", htmlString.substring(0, 1000));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
};
|