sandeepmudhiraj commited on
Commit
f657228
·
verified ·
1 Parent(s): a0efa9c

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +9 -2
  2. app.py +93 -151
  3. requirements.txt +0 -2
  4. settings.yml +60 -0
Dockerfile CHANGED
@@ -7,16 +7,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
7
 
8
  WORKDIR /app
9
 
 
 
 
10
  COPY requirements.txt .
11
  RUN pip install --no-cache-dir -r requirements.txt
12
 
13
  COPY . .
14
 
15
- RUN useradd -m appuser || true
16
- RUN chown -R 1000:1000 /app
 
 
 
17
  USER 1000
18
 
19
  ENV PORT=7860
 
20
  EXPOSE 7860
21
 
22
  CMD ["python", "app.py"]
 
7
 
8
  WORKDIR /app
9
 
10
+ # Install the engine as library
11
+ RUN pip install --no-cache-dir 'searxng>=2024.10'
12
+
13
  COPY requirements.txt .
14
  RUN pip install --no-cache-dir -r requirements.txt
15
 
16
  COPY . .
17
 
18
+ RUN mkdir -p /etc/sxng
19
+ COPY settings.yml /etc/sxng/settings.yml
20
+
21
+ RUN useradd -m appuser 2>/dev/null || true
22
+ RUN chown -R 1000:1000 /app /etc/sxng
23
  USER 1000
24
 
25
  ENV PORT=7860
26
+ ENV SEARXNG_SETTINGS_PATH=/etc/sxng/settings.yml
27
  EXPOSE 7860
28
 
29
  CMD ["python", "app.py"]
app.py CHANGED
@@ -1,19 +1,15 @@
1
  #!/usr/bin/env python3
2
- """
3
- Meta API - Aggregated web data service.
4
- Uses multiple public meta-search endpoints for redundancy.
5
- """
6
  import asyncio
7
- import hashlib
8
- import json
9
  import os
10
- import random
 
11
  import time
12
- from typing import Optional
13
 
14
  import httpx
15
  import uvicorn
16
- from fastapi import FastAPI, Query, Request
17
  from fastapi.middleware.cors import CORSMiddleware
18
  from fastapi.responses import JSONResponse
19
 
@@ -26,107 +22,95 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
29
- # Public SearXNG instances (regularly updated list)
30
- INSTANCES = [
31
- "https://search.bus-hit.me",
32
- "https://search.rhscz.eu",
33
- "https://searx.tiekoetter.com",
34
- "https://search.sapti.me",
35
- "https://searx.be",
36
- "https://search.ononoki.org",
37
- "https://searx.namejeff.xyz",
38
- "https://searx.work",
39
- "https://search.projectsegfau.lt",
40
- "https://searx.oxull.uk",
41
- "https://search.hbubli.cc",
42
- "https://search.mdosch.de",
43
- "https://priv.au",
44
- "https://paulgo.io",
45
- "https://s.mble.dk",
46
- "https://searx.fmhy.net",
47
- "https://searxng.online",
48
- "https://searx.tuxcloud.net",
49
- "https://search.inetol.net",
50
- "https://search.im-in.space",
51
- ]
52
-
53
- # Track instance health
54
- instance_failures: dict[str, int] = {}
55
- instance_last_fail: dict[str, float] = {}
56
- FAILURE_THRESHOLD = 3
57
- COOLDOWN_SECONDS = 300 # 5 min cooldown after failures
58
-
59
- # Simple response cache
60
- _cache: dict[str, tuple[float, dict]] = {}
61
- CACHE_TTL = 300 # 5 minutes
62
-
63
-
64
- def _get_healthy_instances() -> list[str]:
65
- """Get instances that haven't exceeded failure threshold."""
66
- now = time.time()
67
- healthy = []
68
- for inst in INSTANCES:
69
- fails = instance_failures.get(inst, 0)
70
- if fails < FAILURE_THRESHOLD:
71
- healthy.append(inst)
72
- else:
73
- # Check cooldown
74
- last_fail = instance_last_fail.get(inst, 0)
75
- if now - last_fail > COOLDOWN_SECONDS:
76
- instance_failures[inst] = 0
77
- healthy.append(inst)
78
- return healthy if healthy else INSTANCES # fallback to all if none healthy
79
-
80
-
81
- def _record_success(instance: str):
82
- instance_failures[instance] = 0
83
-
84
-
85
- def _record_failure(instance: str):
86
- instance_failures[instance] = instance_failures.get(instance, 0) + 1
87
- instance_last_fail[instance] = time.time()
88
-
89
-
90
- def _cache_key(query: str, **kwargs) -> str:
91
- raw = json.dumps({"q": query, **kwargs}, sort_keys=True)
92
- return hashlib.md5(raw.encode()).hexdigest()
93
 
94
 
95
  @app.get("/")
96
  async def root():
97
- return {"status": "ok", "service": "meta-api", "version": "1.0.0"}
98
 
99
 
100
  @app.get("/health")
101
  async def health():
102
- healthy = _get_healthy_instances()
 
 
 
 
 
 
103
  return {
104
- "status": "healthy",
105
- "available_sources": len(healthy),
106
- "total_sources": len(INSTANCES),
107
  }
108
 
109
 
110
  @app.get("/search")
111
  async def search(
112
- q: str = Query(..., description="Search query"),
113
- format: str = Query("json", description="Output format"),
114
- categories: str = Query("general", description="Search categories"),
115
- language: str = Query("en", description="Language"),
116
- time_range: Optional[str] = Query(None, description="Time range filter"),
117
- pageno: int = Query(1, description="Page number"),
118
  ):
119
- """Perform aggregated search across multiple sources."""
120
- # Check cache
121
- ck = _cache_key(q, categories=categories, language=language, pageno=pageno)
122
- if ck in _cache:
123
- cached_time, cached_data = _cache[ck]
124
- if time.time() - cached_time < CACHE_TTL:
125
- return JSONResponse(content=cached_data)
126
-
127
- healthy = _get_healthy_instances()
128
- random.shuffle(healthy)
129
-
130
  params = {
131
  "q": q,
132
  "format": "json",
@@ -137,62 +121,20 @@ async def search(
137
  if time_range:
138
  params["time_range"] = time_range
139
 
140
- # Try instances with concurrent requests (pick 3 random healthy ones)
141
- candidates = healthy[:5]
142
-
143
- async def try_instance(instance: str) -> Optional[dict]:
144
- url = f"{instance.rstrip('/')}/search"
145
- try:
146
- async with httpx.AsyncClient(timeout=12.0, follow_redirects=True) as client:
147
- resp = await client.get(
148
- url,
149
- params=params,
150
- headers={
151
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
152
- "Accept": "application/json",
153
- },
154
- )
155
- if resp.status_code == 200:
156
- data = resp.json()
157
- if "results" in data and len(data["results"]) > 0:
158
- _record_success(instance)
159
- return data
160
- _record_failure(instance)
161
- return None
162
- except Exception:
163
- _record_failure(instance)
164
- return None
165
-
166
- # Race multiple instances
167
- tasks = [try_instance(inst) for inst in candidates]
168
- results = await asyncio.gather(*tasks, return_exceptions=True)
169
-
170
- for result in results:
171
- if isinstance(result, dict) and result:
172
- # Cache it
173
- _cache[ck] = (time.time(), result)
174
- # Clean old cache entries
175
- now = time.time()
176
- expired = [k for k, (t, _) in _cache.items() if now - t > CACHE_TTL]
177
- for k in expired:
178
- del _cache[k]
179
- return JSONResponse(content=result)
180
-
181
- # If concurrent failed, try remaining instances sequentially
182
- for instance in healthy[5:]:
183
- result = await try_instance(instance)
184
- if result:
185
- _cache[ck] = (time.time(), result)
186
- return JSONResponse(content=result)
187
-
188
- return JSONResponse(
189
- status_code=503,
190
- content={
191
- "error": "All sources temporarily unavailable",
192
- "query": q,
193
- "results": [],
194
- },
195
- )
196
 
197
 
198
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
+ """Meta API — data aggregation service with internal engine."""
 
 
 
3
  import asyncio
 
 
4
  import os
5
+ import sys
6
+ import subprocess
7
  import time
8
+ import threading
9
 
10
  import httpx
11
  import uvicorn
12
+ from fastapi import FastAPI, Query
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from fastapi.responses import JSONResponse
15
 
 
22
  allow_headers=["*"],
23
  )
24
 
25
+ INTERNAL_PORT = 8888
26
+ INTERNAL_URL = f"http://127.0.0.1:{INTERNAL_PORT}"
27
+ _engine_proc = None
28
+
29
+
30
+ def start_engine():
31
+ """Start the internal engine as a background process."""
32
+ global _engine_proc
33
+
34
+ # Write the engine launcher script
35
+ launcher = "/tmp/launcher.py"
36
+ lines = [
37
+ "import sys, os",
38
+ 'os.environ["SEARXNG_SETTINGS_PATH"] = "/etc/sxng/settings.yml"',
39
+ "from searx.webapp import app as webapp",
40
+ "from werkzeug.serving import run_simple",
41
+ f'run_simple("127.0.0.1", {INTERNAL_PORT}, webapp, threaded=True)',
42
+ ]
43
+ with open(launcher, "w") as lf:
44
+ lf.write("\n".join(lines) + "\n")
45
+
46
+ _engine_proc = subprocess.Popen(
47
+ [sys.executable, launcher],
48
+ stdout=subprocess.PIPE,
49
+ stderr=subprocess.PIPE,
50
+ )
51
+ print(f"Engine started (pid={_engine_proc.pid})")
52
+
53
+ # Wait for readiness
54
+ for i in range(60):
55
+ try:
56
+ resp = httpx.get(f"{INTERNAL_URL}/", timeout=2.0)
57
+ if resp.status_code == 200:
58
+ print(f"Engine ready after {i+1}s")
59
+ return True
60
+ except Exception:
61
+ pass
62
+ time.sleep(1)
63
+
64
+ # Check if process died
65
+ if _engine_proc.poll() is not None:
66
+ stderr = _engine_proc.stderr.read().decode()
67
+ print(f"Engine crashed: {stderr[:500]}")
68
+ else:
69
+ print("Engine timeout after 60s")
70
+ return False
71
+
72
+
73
+ @app.on_event("startup")
74
+ async def startup():
75
+ threading.Thread(target=start_engine, daemon=True).start()
76
+
77
+
78
+ @app.on_event("shutdown")
79
+ async def shutdown():
80
+ global _engine_proc
81
+ if _engine_proc:
82
+ _engine_proc.terminate()
 
 
 
 
 
 
83
 
84
 
85
  @app.get("/")
86
  async def root():
87
+ return {"status": "ok", "service": "meta-api", "version": "2.0.0"}
88
 
89
 
90
  @app.get("/health")
91
  async def health():
92
+ try:
93
+ async with httpx.AsyncClient(timeout=5.0) as client:
94
+ resp = await client.get(f"{INTERNAL_URL}/")
95
+ engine_ok = resp.status_code == 200
96
+ except Exception:
97
+ engine_ok = False
98
+
99
  return {
100
+ "status": "healthy" if engine_ok else "starting",
101
+ "engine": "ready" if engine_ok else "initializing",
 
102
  }
103
 
104
 
105
  @app.get("/search")
106
  async def search(
107
+ q: str = Query(..., description="Query"),
108
+ format: str = Query("json"),
109
+ categories: str = Query("general"),
110
+ language: str = Query("en"),
111
+ time_range: str = Query(None),
112
+ pageno: int = Query(1),
113
  ):
 
 
 
 
 
 
 
 
 
 
 
114
  params = {
115
  "q": q,
116
  "format": "json",
 
121
  if time_range:
122
  params["time_range"] = time_range
123
 
124
+ try:
125
+ async with httpx.AsyncClient(timeout=15.0) as client:
126
+ resp = await client.get(f"{INTERNAL_URL}/search", params=params)
127
+ if resp.status_code == 200:
128
+ return JSONResponse(content=resp.json())
129
+ return JSONResponse(
130
+ status_code=resp.status_code,
131
+ content={"error": f"Engine returned {resp.status_code}", "results": []},
132
+ )
133
+ except Exception as e:
134
+ return JSONResponse(
135
+ status_code=503,
136
+ content={"error": str(e), "results": []},
137
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
 
140
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
  fastapi>=0.104.0
2
  uvicorn[standard]>=0.24.0
3
  httpx>=0.25.0
4
- pyyaml>=6.0
5
- lxml>=4.9.0
 
1
  fastapi>=0.104.0
2
  uvicorn[standard]>=0.24.0
3
  httpx>=0.25.0
 
 
settings.yml ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use_default_settings: true
2
+
3
+ server:
4
+ bind_address: "127.0.0.1:8888"
5
+ secret_key: "meta_api_internal_key_2026_v2"
6
+ limiter: false
7
+ image_proxy: false
8
+ public_instance: false
9
+
10
+ search:
11
+ safe_search: 0
12
+ autocomplete: ""
13
+ default_lang: "en"
14
+ formats:
15
+ - html
16
+ - json
17
+
18
+ ui:
19
+ static_use_hash: true
20
+
21
+ enabled_plugins:
22
+ - 'Hash plugin'
23
+ - 'Self Information'
24
+ - 'Tracker URL remover'
25
+
26
+ engines:
27
+ - name: bing
28
+ engine: bing
29
+ shortcut: b
30
+ disabled: false
31
+
32
+ - name: duckduckgo
33
+ engine: duckduckgo
34
+ shortcut: ddg
35
+ disabled: false
36
+
37
+ - name: brave
38
+ engine: brave
39
+ shortcut: br
40
+ disabled: false
41
+
42
+ - name: wikipedia
43
+ engine: wikipedia
44
+ shortcut: wp
45
+ disabled: false
46
+
47
+ - name: yahoo
48
+ engine: yahoo
49
+ shortcut: yh
50
+ disabled: false
51
+
52
+ - name: startpage
53
+ engine: startpage
54
+ shortcut: sp
55
+ disabled: false
56
+
57
+ - name: qwant
58
+ engine: qwant
59
+ shortcut: qw
60
+ disabled: false