vector-merge / app.py
Viral1985's picture
Upload 3 files
c080e7e verified
Raw
History Blame Contribute Delete
6.42 kB
import os, json, gc, pickle, traceback, zipfile, shutil, uuid, logging
from datetime import datetime
from flask import Flask, render_template, request, jsonify, send_file
from werkzeug.utils import secure_filename
import numpy as np
import faiss
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500 MB
app.config['TEMP_FOLDER'] = os.path.join(os.path.expanduser('~'), 'tmp_merge')
os.makedirs(app.config['TEMP_FOLDER'], exist_ok=True)
@app.route('/')
def merge_page():
return render_template('merge.html')
@app.route('/api/merge', methods=['POST'])
def merge_zips():
zip_files = request.files.getlist('files')
out_path = request.form.get('out_path', '/tmp').strip()
out_name = request.form.get('out_name', 'merged_vectorstore').strip()
if out_name.lower().endswith('.zip'):
out_name = out_name[:-4]
if not zip_files:
return jsonify({'error': 'No files uploaded'}), 400
for f in zip_files:
if not f.filename.lower().endswith('.zip'):
return jsonify({'error': f'{f.filename} is not a .zip file'}), 400
os.makedirs(out_path, exist_ok=True)
run_id = uuid.uuid4().hex[:8]
work_dir = os.path.join(app.config['TEMP_FOLDER'], f'merge_{run_id}')
os.makedirs(work_dir, exist_ok=True)
main_index = None
all_documents = []
all_metadata = []
embedding_dim = None
total_merged = 0
errors, skipped = [], []
try:
for idx, zip_file in enumerate(zip_files, 1):
safe_name = secure_filename(zip_file.filename)
zip_save = os.path.join(work_dir, safe_name)
extract_dir = os.path.join(work_dir, f'ex_{idx:02d}')
os.makedirs(extract_dir, exist_ok=True)
zip_file.save(zip_save)
if not zipfile.is_zipfile(zip_save):
errors.append(f'{safe_name}: not a valid zip β€” skipped')
continue
with zipfile.ZipFile(zip_save, 'r') as zf:
zf.extractall(extract_dir)
# Locate the three required files
index_file = data_file = meta_file = None
for root, dirs, fnames in os.walk(extract_dir):
for fn in fnames:
full = os.path.join(root, fn)
if fn == 'faiss.index' and index_file is None: index_file = full
if fn == 'data.pkl' and data_file is None: data_file = full
if fn == 'meta.json' and meta_file is None: meta_file = full
if not index_file or not data_file:
errors.append(f'{safe_name}: missing faiss.index/data.pkl β€” skipped')
continue
# Load meta.json (optional, we can create it later)
meta = {}
if meta_file:
with open(meta_file, 'r') as f:
meta = json.load(f)
try:
sub_index = faiss.read_index(index_file)
with open(data_file, 'rb') as f:
sub_data = pickle.load(f)
except Exception as e:
errors.append(f'{safe_name}: load error β€” {e}')
continue
sub_docs = sub_data.get('documents', [])
sub_meta = sub_data.get('metadata', [])
n = sub_index.ntotal
if n == 0:
skipped.append(f'{safe_name}: empty index β€” skipped')
continue
if n != len(sub_docs) or n != len(sub_meta):
errors.append(f'{safe_name}: index/pkl mismatch ({n} vectors, {len(sub_docs)} docs, {len(sub_meta)} meta) β€” skipped')
continue
# Dimension check
dim = sub_index.d
if embedding_dim is None:
embedding_dim = dim
main_index = faiss.IndexFlatL2(dim)
elif dim != embedding_dim:
errors.append(f'{safe_name}: dimension mismatch (got {dim}, expected {embedding_dim}) β€” skipped')
continue
# Extract vectors
vectors = np.zeros((n, dim), dtype=np.float32)
sub_index.reconstruct_n(0, n, vectors)
main_index.add(vectors)
all_documents.extend(sub_docs)
all_metadata.extend(sub_meta)
total_merged += n
logger.info(f' βœ“ {safe_name}: +{n} vectors (total {total_merged})')
del sub_index, vectors, sub_docs, sub_meta, sub_data
gc.collect()
if total_merged == 0:
return jsonify({'error': 'No vectors merged', 'errors': errors, 'skipped': skipped}), 400
# Prepare output zip
merged_dir = os.path.join(work_dir, 'merged')
os.makedirs(merged_dir, exist_ok=True)
# Write faiss.index
index_out = os.path.join(merged_dir, 'faiss.index')
faiss.write_index(main_index, index_out)
# Write data.pkl
data_out = os.path.join(merged_dir, 'data.pkl')
with open(data_out, 'wb') as f:
pickle.dump({'documents': all_documents, 'metadata': all_metadata}, f)
# Write fresh meta.json
new_meta = {
'version': 2,
'similarity': 'cosine', # safe default; adjust if you know the real one
'embedding_dim': embedding_dim,
'chunk_count': total_merged
}
meta_out = os.path.join(merged_dir, 'meta.json')
with open(meta_out, 'w') as f:
json.dump(new_meta, f)
# Package into zip
final_zip = os.path.join(out_path, out_name + '.zip')
with zipfile.ZipFile(final_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.write(index_out, 'faiss.index')
zf.write(data_out, 'data.pkl')
zf.write(meta_out, 'meta.json')
return send_file(final_zip, as_attachment=True, download_name=os.path.basename(final_zip))
except Exception as e:
logger.error(f'Merge error: {e}')
logger.error(traceback.format_exc())
return jsonify({'error': str(e)}), 500
finally:
if os.path.exists(work_dir):
shutil.rmtree(work_dir, ignore_errors=True)
if __name__ == '__main__':
port = int(os.environ.get('PORT', 7860))
logger.info(f'βœ… Starting merge server on port {port}')
app.run(host='0.0.0.0', port=port, debug=False)