[MAFM] 구현

JUJU·2024년 9월 26일
0

프로젝트

목록 보기
22/26

지능형 파일 관리 시스템

이전 포스트에서 임의의 자연어를 Embedding하여 Vector DB에 저장하는 플로우를 구현했다.
다음 과정을 구현하기 전에, 프로젝트의 전체 흐름을 다시 짚어보도록 하자.


✏️ What is Next?

프로젝트의 전체 흐름을 다시 짚어보자.

우리가 개발하는 것은 지능형 파일 관리 시스템(Multi-Agent File Management)이다.

일반적인 파일 관리자와 무엇이 다른가?
➜ Multi-Agent를 사용하여 파일 검색 기능을 업그레이드 할 것이다.

ex) "AI와 VectorDB"에 관한 파일을 찾는다고 가정해보자.
일반적인 파일 관리자는 정확히 "AI와 VectorDB" 라는 워딩이 들어간 파일을 찾을 것이다.
루트에서부터 파일을 검색하기 시작하면, 매우 오랜 시간이 소요될 것이다.
또한, 정확히 "AI와 VectorDB"라는 워딩이 들어가 있는 경우만 결과로써 도출할 것이다.

사용자가 원하는 것은 "AI와 Vector"라는 워딩 그 자체가 아니다.
"AI"라는 주제와 "VectorDB"라는 주제에 대해 서술한 파일을 원하는 것이다.

MAFM을 사용하면 위의 문제를 해결할 수 있다.


VectorDB를 사용하면 사용자가 원하는 쿼리와 연관성이 높은 파일들을 검색할 수 있다.
하지만, VectorDB 하나에 시스템의 모든 파일들을 저장하는 것은 어렵다.
각 Vector의 차원 수가 너무 많아지고 속도도 느려지기 때문이다.

여기서 Multi-Agent를 도입한다!
각 Agent가 하나의 디렉토리와 하나의 VectorDB를 관리하도록 만드는 것이다.
이렇게 하면 VectorDB의 차원 수 부담을 줄일 수 있고 검색 성능도 향상시킬 수 있다. (AI를 사용해서 쿼리의 핵심 내용만 추출하기 때문)


■ 요구사항

MAFM을 구현하기 위해 개발해야 하는 기능들에 대해서 생각해 보자.

사용자가 새로운 파일을 생성하고 저장한다.
-> 파일을 500 Bytes 크기로 쪼갠다. (너무 크면 Embedding이 어려움)
-> 쪼개진 파일의 내용을 읽는다
-> 각 내용을 Embedding 한다.
-> VectorDB에 저장한다.(이 때, 하나의 파일에서 쪼개진 파일들은 모두 같은 id를 가져야 한다.)
-> 디렉토리에 변경사항이 발생하였으므로 변경 사항을 저장해야 한다.

변경 사항 저장은 어떻게?
SQLite를 사용한다.
아래와 같은 테이블 구조를 활용해서 디렉토리의 구조와 파일 경로를 표시한다.


2개의 테이블로 분리

  1. 파일 경로를 나타내는 테이블
record_id(PK)idfile_pathis_dir
1117/User/Downloads/True
2118/User/Downloads/a.txtFalse
3119/User/Downloads/a.txtFalse

  1. 디렉토리 구조를 나타내는 테이블
record_id(PK)iddir_pathparent_dir_path
1117/User/Downloads//User/
2250/User/Downloads/folder1//User/Downloads/
3333/User/Downloads/folder1/nested/User/Downloads/folder1

각각에 대한 CRUD 구현 필요

사용자가 파일을 검색한다.
-> 검색 쿼리를 각 Agent에게 전달
-> 각 Agent는 자신의 Vector DB에서 검색
-> 나온 결과(file Path)를 반환
-> 반환된 file Path를 기반으로 Symbolic Link를 생성
-> 여러개 파일이 검색되었을 수 있으므로, 생성된 Symbolic Link들을 모두 모아 임시 디렉토리(temp)에 저장
-> 해당 임시 디렉토리를 사용자에게 보여줌


■ What is next

다음으로 구현해야 할 것은 무엇일까?

  1. 디렉토리의 구조와 파일 경로를 저장하는 코드가 필요하다.
    • 특정 디렉토리를 루트로 설정하고, 해당 디렉토리부터 하위의 모든 파일을 MAFM에서 관리한다.
    • SQLite를 사용해서 구현한다.
    • 루트의 하위 파일들을 탐색해서 저장해야 하는데, 이것을 파이썬과 C언어로 각각 테스트 하여 성능차이를 확인해봐야 한다.



1. 디렉토리 구조/파일 경로 저장

디렉토리 구조와 파일 경로를 DB에 저장하는 코드를 구현해보자.


DB CRUD

우선, Python - SQLite 간의 CRUD 기능을 만들어야 한다.

# DB 연결 및 초기화 코드
def initialize_database(db_name='filesystem.db'):
    # 기존에 db가 존재하면 날림
    if os.path.exists(db_name):
        os.remove(db_name)

    # 데이터베이스 파일에 연결
    connection = sqlite3.connect('filesystem.db')

    # 커서 생성
    # 커서는 SQL 문을 실행하고 결과를 처리하는 데 사용되는 객체이다.
    # cursor.execute() 메소드를 사용해서 데이터베이스에 대한 SQL 쿼리를 실행할 수 있다.
    cursor = connection.cursor()

    # 첫 번째 테이블(file_info) 생성
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS file_info (
            record_id INTEGER PRIMARY KEY AUTOINCREMENT,
            id TEXT NOT NULL,
            file_path TEXT NOT NULL,
            is_dir INTEGER NOT NULL
        )
    ''')

    # 두 번째 테이블(directory_structure) 생성
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS directory_structure (
            record_id INTEGER PRIMARY KEY AUTOINCREMENT,
            id TEXT NOT NULL,
            dir_path TEXT NOT NULL,
            parent_dir_path TEXT
        )
    ''')

    # 변경 사항 저장
    connection.commit()
    connection.close()
  • cursor는 SQL 문을 실행하고 결과를 처리하는 데 사용되는 객체이다.

# CREATE 함수 - 데이터 삽입
def insert_file_info(id, file_path, is_dir, db_name='filesystem.db'):
    connection = sqlite3.connect(db_name)
    cursor = connection.cursor()
    cursor.execute('''
        INSERT INTO file_info (id, file_path, is_dir)
        VALUES (?, ?, ?)
    ''', (id, file_path, is_dir))
    connection.commit()
    connection.close()

위와 같은 방식으로 CRUD를 모두 구현하였다.


DB에 데이터 저장

앞서 만든 CRUD 기능을 사용해서 filesystem DB에 디렉토리 구조/파일 경로를 저장해야 한다.
또한, 각각의 디렉토리 Vector DB에 파일의 내용을 저장해야 한다.

  • filesystem.db 는 디렉토리의 구조와 파일 경로를 저장하는 DB이다.
  • [디렉토리 이름].db 는 각 디렉토리에 대응되는 VectorDB로, 파일의 내용 자체를 임베딩해서 저장한다.
  1. 지정한 루트 아래에 존재하는 모든 디렉토리를 탐색하면서, 존재하는 모든 파일 경로를 DB에 저장한다.
    • 디렉토리도 파일이므로 같이 저장한다.
  2. 파일의 경로를 저장한 뒤에는 파일을 읽어서 Embedding하여 Vector화 한다.
  3. Vector화 된 파일을 각 VectorDB에 저장한다.

위의 과정을 파이썬과 C언어로 각각 구현하여 성능을 체크한다.

# python으로 구현한 코드
def start_command_python(root):
    # 시작 시간 기록
    start_time = time.time()

    # SQLite DB 연결 및 초기화
    try:
        initialize_database("filesystem.db")
    except Exception as e:
        print(f"Error initializing database: {e}")
        return

    id = 1

    # root 자체는 os.walk(root)에 포함되지 않음 -> 따로 처리 필요
    try:
        initialize_vector_db(root + ".db")
    except Exception as e:
        print(f"Error initializing vector DB for root: {e}")
        return

    print(root)
    insert_file_info(id, root, 1, "filesystem.db")

    # 루트의 부모 디렉토리 찾기
    last_slash_index = root.rfind("/")
    if last_slash_index != -1:
        root_parent = root[:last_slash_index]

    insert_directory_structure(id, root, root_parent, "filesystem.db")
    id += 1

    # 디렉터리 재귀 탐색
    for dirpath, dirnames, filenames in os.walk(root):
        # 디렉터리 정보 삽입
        for dirname in dirnames:
            full_path = os.path.join(dirpath, dirname)

            try:
                initialize_vector_db(full_path + ".db")
            except Exception as e:
                print(f"Error initializing vector DB for directory: {e}")
                continue

            print(full_path)
            insert_file_info(id, full_path, 1, "filesystem.db")
            insert_directory_structure(id, full_path, dirpath, "filesystem.db")
            id += 1

        # 파일 정보 삽입 및 벡터 DB에 저장
        for filename in filenames:
            # 비밀 파일(파일 이름이 .으로 시작)과 .db 파일 제외
            if filename.startswith(".") or filename.endswith(".db"):
                continue

            full_path = os.path.join(dirpath, filename)
            print(full_path)

            # 파일 정보 삽입
            insert_file_info(id, full_path, 0, "filesystem.db")

            file_chunks = []

            # 파일 데이터를 500Bytes씩 읽기
            try:
                with open(full_path, "rb") as file:
                    while True:
                        chunk = file.read(500)
                        if not chunk:
                            break
                        file_chunks.append(
                            chunk.decode("utf-8", errors="ignore")
                        )  # 바이너리 데이터를 문자열로 변환
            except Exception as e:
                print(f"Failed to read file data for {full_path}: {e}")
                continue

            # 각 디렉토리의 벡터 DB에 해당 파일 내용을 저장
            save(dirpath + ".db", id, file_chunks)

            id += 1

    # 종료 시간 기록
    end_time = time.time()

    # 걸린 시간 계산
    elapsed_time = end_time - start_time
    print(f"작업에 걸린 시간: {elapsed_time:.4f} 초")

MAFM_test 디렉토리로 테스트 한 경우 15.5217초 걸림


c를 사용해서 위의 로직을 구현하려면, 우선 해당 디렉토리에 존재하는 모든 파일 리스트를 얻고 각 파일의 내용을 Read 하는 코드가 필요하다.

// get_file_data: 주어진 파일 경로에 대한 정보를 읽고 필요한 데이터를 반환
// 입력: path (파일 경로)
// 출력: 파일 정보 배열 (파일 경로, 파일 이름, 파일 내용 조각)
/* 파일 정보 배열의 구조:
 * data[0]: 파일의 전체 경로 (char*)
 * data[1]: 파일의 이름 (char*)
 * data[2], data[3], ...: 파일 내용을 일정 크기(chunkSize)로 나눈 조각들 (char*)
 * 마지막 data[idx + 1]: NULL 포인터 (배열의 끝을 알리기 위해)
*/
char** get_file_data(const char* path) {
    // 파일을 읽기 모드로 엽니다.
    FILE *file = fopen(path, "rb");
    if (!file) {
        perror("Failed to open file"); // 파일 열기 실패 시 오류 메시지 출력
        return NULL; // 실패 시 NULL 반환
    }

    // 파일 이름을 가져옵니다.
    char *fname = get_filename(path);
    if (!fname) {
        perror("Failed to get filename"); // 파일 이름을 가져오는 데 실패하면 오류 메시지 출력
        fclose(file); // 파일 닫기
        return NULL; // 실패 시 NULL 반환
    }

    // 파일이 이미지 또는 비디오인지 확인합니다.
    int is_image_or_video_flag = is_image_or_video(path);

    // 이미지 또는 비디오 파일일 경우
    if (is_image_or_video_flag) {
        char **data = malloc(sizeof(char *) * 3); // data 배열에 3개의 포인터 공간을 할당합니다.
        if (!data) {
            perror("Failed to allocate memory for data array"); // 메모리 할당 실패 시 오류 메시지 출력
            free(fname); // 파일 이름 메모리 해제
            fclose(file); // 파일 닫기
            return NULL; // 실패 시 NULL 반환
        }
        data[0] = strdup(path); // 경로 복사
        if (!data[0]) {
            perror("Failed to duplicate path"); // 경로 복사 실패 시 오류 메시지 출력
            free(data); // data 배열 해제
            free(fname); // 파일 이름 해제
            fclose(file); // 파일 닫기
            return NULL; // 실패 시 NULL 반환
        }
        data[1] = fname; // 파일 이름 저장
        data[2] = NULL; // 마지막에 NULL 포인터 설정 (배열의 끝을 알리기 위해)
        fclose(file); // 파일 닫기
        return data; // data 배열 반환
    }

    // 일반 파일일 경우, 초기 배열 크기를 설정합니다.
    int maxChunks = 4;
    char **data = (char **)malloc(sizeof(char *) * maxChunks); // 초기 크기 4로 data 배열 할당
    if (!data) {
        perror("Failed to allocate memory for data array"); // 메모리 할당 실패 시 오류 메시지 출력
        free(fname); // 파일 이름 해제
        fclose(file); // 파일 닫기
        return NULL; // 실패 시 NULL 반환
    }

    data[0] = strdup(path); // 파일 경로 복사
    if (!data[0]) {
        perror("Failed to duplicate path"); // 파일 경로 복사 실패 시 오류 메시지 출력
        free(data); // data 배열 해제
        free(fname); // 파일 이름 해제
        fclose(file); // 파일 닫기
        return NULL; // 실패 시 NULL 반환
    }

    data[1] = fname; // 파일 이름 저장

    int idx = 2; // 데이터 조각을 저장할 인덱스 시작 (0과 1은 경로와 이름)
    int chunkSize = 500; // 각 조각의 크기 (500바이트)
    int bytesRead;

    // 파일 내용을 500바이트씩 읽습니다.
    while (1) {
        if (idx >= maxChunks) {
            // 현재 배열의 크기가 부족할 경우 크기를 2배로 늘립니다.
            maxChunks *= 2;
            char **temp = realloc(data, maxChunks * sizeof(char *));
            if (temp == NULL) {
                perror("Failed to reallocate memory for data array"); // 메모리 재할당 실패 시 오류 메시지 출력
                // 이미 할당된 메모리 해제
                for (int i = 0; i < idx; i++) {
                    free(data[i]);
                }
                free(data);
                fclose(file);
                return NULL; // 실패 시 NULL 반환
            }
            data = temp; // 재할당된 메모리 주소로 업데이트
        }

        // 새로운 조각을 위한 메모리 할당
        data[idx] = (char *)malloc(chunkSize * sizeof(char));
        if (data[idx] == NULL) {
            perror("Failed to allocate memory for chunk"); // 메모리 할당 실패 시 오류 메시지 출력
            // 이미 할당된 메모리 해제
            for (int i = 0; i < idx; i++) {
                free(data[i]);
            }
            free(data);
            fclose(file);
            return NULL; // 실패 시 NULL 반환
        }

        // 파일에서 chunkSize만큼 읽기
        bytesRead = fread(data[idx], 1, chunkSize, file);
        if (bytesRead > 0) {
            if (bytesRead < chunkSize) {
                // 만약 읽은 바이트가 chunkSize보다 적다면 메모리 크기 조정
                char *adjusted = realloc(data[idx], bytesRead);
                if (adjusted) {
                    data[idx] = adjusted;
                }
            }
            idx++;
        }
        if (bytesRead < chunkSize) {
            if (feof(file)) {
                break; // 파일 끝에 도달하면 종료
            } else if (ferror(file)) {
                perror("Error reading file"); // 파일 읽기 중 오류 발생 시 메시지 출력
                // 이미 할당된 메모리 해제
                for (int i = 0; i <= idx; i++) {
                    free(data[i]);
                }
                free(data);
                fclose(file);
                return NULL; // 실패 시 NULL 반환
            }
        }
    }

    // 마지막에 NULL 포인터 설정 (배열의 끝을 알리기 위해)
    if (idx < maxChunks) {
        data[idx] = NULL;
    } else {
        // 추가 공간이 필요하면 재할당하여 NULL 포인터 추가
        char **temp = realloc(data, (idx + 1) * sizeof(char *));
        if (temp == NULL) {
            perror("Failed to reallocate memory for terminating NULL pointer"); // 메모리 재할당 실패 시 오류 메시지 출력
            // 이미 할당된 메모리 해제
            for (int i = 0; i < idx; i++) {
                free(data[i]);
            }
            free(data);
            fclose(file);
            return NULL; // 실패 시 NULL 반환
        }
        data = temp;
        data[idx] = NULL;
    }

    fclose(file); // 파일 닫기
    return data; // 파일 정보와 조각들을 포함한 data 배열 반환
}
# C를 사용해서 구현한 코드

C언어로 변경 -> 약 10%의 속도 증진

profile
개발자 지망생

0개의 댓글

관련 채용 정보